From 24503af2b264f1e19402292044d74cafc0b71aa2 Mon Sep 17 00:00:00 2001 From: Basit Chonka Date: Tue, 10 Dec 2024 20:21:46 +0100 Subject: [PATCH 1/6] remove combobox clone --- package-lock.json | 288 +++- packages/compass-components/package.json | 4 +- .../combobox-with-custom-option.tsx | 20 +- .../combobox-with-custom-options.spec.tsx | 2 +- .../src/components/combobox/Chip.tsx | 283 ---- .../components/combobox/Combobox.styles.ts | 333 ---- .../src/components/combobox/Combobox.tsx | 1455 ----------------- .../src/components/combobox/Combobox.types.ts | 378 ----- .../components/combobox/ComboboxContext.tsx | 39 - .../src/components/combobox/ComboboxGroup.tsx | 80 - .../combobox/ComboboxMenu/ComboboxMenu.tsx | 163 -- .../combobox/ComboboxMenu/Menu.styles.ts | 133 -- .../components/combobox/ComboboxOption.tsx | 352 ---- .../src/components/combobox/Menu.styles.ts | 112 -- .../src/components/combobox/README.md | 9 - .../src/components/combobox/index.ts | 10 - .../combobox/utils/OptionObjectUtils.ts | 44 - .../combobox/utils/flattenChildren.tsx | 52 - .../combobox/utils/getNameAndValue.ts | 25 - .../src/components/combobox/utils/index.ts | 8 - .../src/components/combobox/utils/wrapJSX.tsx | 57 - .../src/components/leafygreen.tsx | 12 + packages/compass-components/src/index.ts | 1 - 23 files changed, 272 insertions(+), 3588 deletions(-) delete mode 100644 packages/compass-components/src/components/combobox/Chip.tsx delete mode 100644 packages/compass-components/src/components/combobox/Combobox.styles.ts delete mode 100644 packages/compass-components/src/components/combobox/Combobox.tsx delete mode 100644 packages/compass-components/src/components/combobox/Combobox.types.ts delete mode 100644 packages/compass-components/src/components/combobox/ComboboxContext.tsx delete mode 100644 packages/compass-components/src/components/combobox/ComboboxGroup.tsx delete mode 100644 packages/compass-components/src/components/combobox/ComboboxMenu/ComboboxMenu.tsx delete mode 100644 packages/compass-components/src/components/combobox/ComboboxMenu/Menu.styles.ts delete mode 100644 packages/compass-components/src/components/combobox/ComboboxOption.tsx delete mode 100644 packages/compass-components/src/components/combobox/Menu.styles.ts delete mode 100644 packages/compass-components/src/components/combobox/README.md delete mode 100644 packages/compass-components/src/components/combobox/index.ts delete mode 100644 packages/compass-components/src/components/combobox/utils/OptionObjectUtils.ts delete mode 100644 packages/compass-components/src/components/combobox/utils/flattenChildren.tsx delete mode 100644 packages/compass-components/src/components/combobox/utils/getNameAndValue.ts delete mode 100644 packages/compass-components/src/components/combobox/utils/index.ts delete mode 100644 packages/compass-components/src/components/combobox/utils/wrapJSX.tsx diff --git a/package-lock.json b/package-lock.json index c3bbc71a0cb..8f7f96f5a45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5200,6 +5200,23 @@ "@leafygreen-ui/leafygreen-provider": "^3.1.10" } }, + "node_modules/@leafygreen-ui/chip": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/chip/-/chip-1.2.1.tgz", + "integrity": "sha512-LkixgBVmxZCfhKYowrMhTT6ivYNNV1Pjdwhkl1UgaWAQMZIFOGfUPgwVcqH+1AVJ98fjZ43OOFe8qfrpNnc00w==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/icon": "^12.5.4", + "@leafygreen-ui/inline-definition": "^6.0.15", + "@leafygreen-ui/lib": "^13.6.0", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/tokens": "^2.9.0" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^3.1.12" + } + }, "node_modules/@leafygreen-ui/code": { "version": "14.3.1", "resolved": "https://registry.npmjs.org/@leafygreen-ui/code/-/code-14.3.1.tgz", @@ -5229,6 +5246,98 @@ "@leafygreen-ui/leafygreen-provider": "^3.1.11" } }, + "node_modules/@leafygreen-ui/combobox": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/combobox/-/combobox-9.1.6.tgz", + "integrity": "sha512-C+Le23MtRTPaDZqwA08PC0GeTeNNWsW3leOREutBwN+a6LQPMkvA0B/S4mTvZ4/GCbo9gPnNkwtu6FGfGiBnlA==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/checkbox": "^13.1.2", + "@leafygreen-ui/chip": "^1.2.1", + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/form-field": "^1.2.5", + "@leafygreen-ui/hooks": "^8.2.1", + "@leafygreen-ui/icon": "^12.5.4", + "@leafygreen-ui/icon-button": "^15.0.23", + "@leafygreen-ui/input-option": "^2.0.2", + "@leafygreen-ui/lib": "^13.6.1", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/popover": "^11.4.0", + "@leafygreen-ui/tokens": "^2.9.0", + "@leafygreen-ui/typography": "^19.2.1", + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "polished": "^4.2.2" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^3.1.12" + } + }, + "node_modules/@leafygreen-ui/combobox/node_modules/@leafygreen-ui/checkbox": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/checkbox/-/checkbox-13.1.2.tgz", + "integrity": "sha512-rdn55oDiywyk/t3wKnJKbzDn6CUtCCSm4PQF6t4svZWVaHvNzDgTDjHy5D1s8MYpFQbqhsWbJhf17tpRrzY/Mw==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/a11y": "^1.4.13", + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/hooks": "^8.1.4", + "@leafygreen-ui/lib": "^13.4.0", + "@leafygreen-ui/palette": "^4.0.10", + "@leafygreen-ui/tokens": "^2.5.2", + "@leafygreen-ui/typography": "^19.0.0", + "@lg-tools/test-harnesses": "^0.1.2", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^3.1.12" + } + }, + "node_modules/@leafygreen-ui/combobox/node_modules/@leafygreen-ui/input-option": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/input-option/-/input-option-2.0.2.tgz", + "integrity": "sha512-GD3TX/5uF6NMdlcOt89jg7NXrN43ZAm+TEg/84NT9Mpdik9pw44Nznhv/BD/jXaWpxPXlDQzq7ReAOi7WtUujg==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/a11y": "^1.5.0", + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/lib": "^13.6.1", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/polymorphic": "^2.0.0", + "@leafygreen-ui/tokens": "^2.9.0", + "@leafygreen-ui/typography": "^19.2.1" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^3.1.12" + } + }, + "node_modules/@leafygreen-ui/combobox/node_modules/@leafygreen-ui/polymorphic": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/polymorphic/-/polymorphic-2.0.2.tgz", + "integrity": "sha512-OjP+hPG/cwADShcGa1SZdm51G2wVpbNqpU0B3GonEAvGLcAvG4LDMXa7BWo3GDliNkPtVMS86w0eZzEDmLfKmQ==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/lib": "^13.6.0", + "lodash": "^4.17.21" + } + }, + "node_modules/@leafygreen-ui/combobox/node_modules/@leafygreen-ui/typography": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/typography/-/typography-19.3.0.tgz", + "integrity": "sha512-pgTRcc4usW/S9nDDzkf5Ac/JPEybhWtOnDpmrp99mAJHM6tH48Pd1HjRNHWjn6bnh0nXWjwANXX1ZEe+8ggCNg==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/icon": "^12.6.0", + "@leafygreen-ui/lib": "^13.6.1", + "@leafygreen-ui/palette": "^4.0.10", + "@leafygreen-ui/polymorphic": "^2.0.0", + "@leafygreen-ui/tokens": "^2.9.0" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^3.1.12" + } + }, "node_modules/@leafygreen-ui/confirmation-modal": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@leafygreen-ui/confirmation-modal/-/confirmation-modal-5.2.0.tgz", @@ -5300,9 +5409,10 @@ } }, "node_modules/@leafygreen-ui/form-field": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/form-field/-/form-field-1.2.4.tgz", - "integrity": "sha512-cMmeyjsOjEDott5wbdS7pc2EiUa0sNKhTcPlh5DmZy3jDUL1FB5XXvnxlw6xmieZJxtpFBIguaPpToIggLaLrA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/form-field/-/form-field-1.2.5.tgz", + "integrity": "sha512-XH7vJZbgn6wnS7Wv0DpNqcL8q0qPqxHsrVBnqk+iKlnGjCjo1GFzngjOIHODUymEfWRJERrxKO6z8FsSof0GsQ==", + "license": "Apache-2.0", "dependencies": { "@leafygreen-ui/emotion": "^4.0.8", "@leafygreen-ui/hooks": "^8.1.3", @@ -5366,11 +5476,12 @@ } }, "node_modules/@leafygreen-ui/hooks": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-8.1.3.tgz", - "integrity": "sha512-UAHii7T+g8h8sSzogqUgIid64bbKPHGihAAoBpNzbNsjqFllYVC0FpF59jQeL6tCYd32C2KatWOvhYheBf1hsA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-8.3.0.tgz", + "integrity": "sha512-6z+QdFKB9UniDs9gwwXcWalHAypxEaZG7DOh/o9VSqh+yrfQKm+muPzU/oo40ts72trv1tBVv1pU4+X2oBbzmw==", + "license": "Apache-2.0", "dependencies": { - "@leafygreen-ui/lib": "^13.3.0", + "@leafygreen-ui/lib": "^13.8.1", "lodash": "^4.17.21" } }, @@ -5384,9 +5495,10 @@ } }, "node_modules/@leafygreen-ui/icon-button": { - "version": "15.0.22", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/icon-button/-/icon-button-15.0.22.tgz", - "integrity": "sha512-o9+gSUfL5ZE6g05m89vv0BRtD+qcfOpfgbuusN5KXdvbAKPgUaweySFl6rMHgSybfdMM1E36rmxyyCwEo7Vahw==", + "version": "15.0.23", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/icon-button/-/icon-button-15.0.23.tgz", + "integrity": "sha512-UnvHugYqr/FFTmbiascoxM0QMuJogP8d3H413ftCQM3jx8F65JbOwSV2X7QWoesKBTzppAfySbw+zMNihP2L1w==", + "license": "Apache-2.0", "dependencies": { "@leafygreen-ui/a11y": "^1.4.13", "@leafygreen-ui/box": "^3.1.9", @@ -5418,17 +5530,18 @@ } }, "node_modules/@leafygreen-ui/inline-definition": { - "version": "6.0.14", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/inline-definition/-/inline-definition-6.0.14.tgz", - "integrity": "sha512-vCfSF1Lr8O4sm8f7w9rTflVyJRjF3Tyrtppr9OSfEPTDDlla+tiuSyvrMUty3xfdomc6JEGyumdozvjyU9dFsg==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/inline-definition/-/inline-definition-6.0.15.tgz", + "integrity": "sha512-Lv6j68szWlAol8CU+CI2ip3AcxoOvX/tizsWvNg46atyrkQg4cw0ow/PQMFpO0BwzCCEZK0hGXxbjrQmDWfUvg==", + "license": "Apache-2.0", "dependencies": { - "@leafygreen-ui/emotion": "^4.0.7", - "@leafygreen-ui/lib": "^13.0.0", - "@leafygreen-ui/palette": "^4.0.7", - "@leafygreen-ui/tooltip": "^11.0.0" + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/lib": "^13.3.0", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/tooltip": "^11.0.3" }, "peerDependencies": { - "@leafygreen-ui/leafygreen-provider": "^3.1.10" + "@leafygreen-ui/leafygreen-provider": "^3.1.12" } }, "node_modules/@leafygreen-ui/input-option": { @@ -5459,9 +5572,10 @@ } }, "node_modules/@leafygreen-ui/lib": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/lib/-/lib-13.7.0.tgz", - "integrity": "sha512-R+2br+QrCABPefv5SD4DOAduIveoVxFtSRqk51frjLyATHLUhg7SwV783KJ0ipofCfsLdae2CZRSzT7MAVbSEA==", + "version": "13.8.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/lib/-/lib-13.8.2.tgz", + "integrity": "sha512-UxtZauF0rsB2dT0dsFYadcs9qa22Wk3PJaSXOCoI8BRPxyV8H4H6B+FQuFjCeLpKWFYOGLee9di3Xsqd4ewa8Q==", + "license": "Apache-2.0", "dependencies": { "@storybook/csf": "^0.1.0", "lodash": "^4.17.21", @@ -43419,6 +43533,7 @@ "@leafygreen-ui/card": "^10.0.6", "@leafygreen-ui/checkbox": "^12.1.1", "@leafygreen-ui/code": "^14.3.1", + "@leafygreen-ui/combobox": "^9.1.6", "@leafygreen-ui/confirmation-modal": "^5.2.0", "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/guide-cue": "^5.0.6", @@ -52948,6 +53063,19 @@ "react-transition-group": "^4.4.5" } }, + "@leafygreen-ui/chip": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/chip/-/chip-1.2.1.tgz", + "integrity": "sha512-LkixgBVmxZCfhKYowrMhTT6ivYNNV1Pjdwhkl1UgaWAQMZIFOGfUPgwVcqH+1AVJ98fjZ43OOFe8qfrpNnc00w==", + "requires": { + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/icon": "^12.5.4", + "@leafygreen-ui/inline-definition": "^6.0.15", + "@leafygreen-ui/lib": "^13.6.0", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/tokens": "^2.9.0" + } + }, "@leafygreen-ui/code": { "version": "14.3.1", "resolved": "https://registry.npmjs.org/@leafygreen-ui/code/-/code-14.3.1.tgz", @@ -52974,6 +53102,83 @@ "polished": "^4.2.2" } }, + "@leafygreen-ui/combobox": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/combobox/-/combobox-9.1.6.tgz", + "integrity": "sha512-C+Le23MtRTPaDZqwA08PC0GeTeNNWsW3leOREutBwN+a6LQPMkvA0B/S4mTvZ4/GCbo9gPnNkwtu6FGfGiBnlA==", + "requires": { + "@leafygreen-ui/checkbox": "^13.1.2", + "@leafygreen-ui/chip": "^1.2.1", + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/form-field": "^1.2.5", + "@leafygreen-ui/hooks": "^8.2.1", + "@leafygreen-ui/icon": "^12.5.4", + "@leafygreen-ui/icon-button": "^15.0.23", + "@leafygreen-ui/input-option": "^2.0.2", + "@leafygreen-ui/lib": "^13.6.1", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/popover": "^11.4.0", + "@leafygreen-ui/tokens": "^2.9.0", + "@leafygreen-ui/typography": "^19.2.1", + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "polished": "^4.2.2" + }, + "dependencies": { + "@leafygreen-ui/checkbox": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/checkbox/-/checkbox-13.1.2.tgz", + "integrity": "sha512-rdn55oDiywyk/t3wKnJKbzDn6CUtCCSm4PQF6t4svZWVaHvNzDgTDjHy5D1s8MYpFQbqhsWbJhf17tpRrzY/Mw==", + "requires": { + "@leafygreen-ui/a11y": "^1.4.13", + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/hooks": "^8.1.4", + "@leafygreen-ui/lib": "^13.4.0", + "@leafygreen-ui/palette": "^4.0.10", + "@leafygreen-ui/tokens": "^2.5.2", + "@leafygreen-ui/typography": "^19.0.0", + "@lg-tools/test-harnesses": "^0.1.2", + "react-transition-group": "^4.4.5" + } + }, + "@leafygreen-ui/input-option": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/input-option/-/input-option-2.0.2.tgz", + "integrity": "sha512-GD3TX/5uF6NMdlcOt89jg7NXrN43ZAm+TEg/84NT9Mpdik9pw44Nznhv/BD/jXaWpxPXlDQzq7ReAOi7WtUujg==", + "requires": { + "@leafygreen-ui/a11y": "^1.5.0", + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/lib": "^13.6.1", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/polymorphic": "^2.0.0", + "@leafygreen-ui/tokens": "^2.9.0", + "@leafygreen-ui/typography": "^19.2.1" + } + }, + "@leafygreen-ui/polymorphic": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/polymorphic/-/polymorphic-2.0.2.tgz", + "integrity": "sha512-OjP+hPG/cwADShcGa1SZdm51G2wVpbNqpU0B3GonEAvGLcAvG4LDMXa7BWo3GDliNkPtVMS86w0eZzEDmLfKmQ==", + "requires": { + "@leafygreen-ui/lib": "^13.6.0", + "lodash": "^4.17.21" + } + }, + "@leafygreen-ui/typography": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/typography/-/typography-19.3.0.tgz", + "integrity": "sha512-pgTRcc4usW/S9nDDzkf5Ac/JPEybhWtOnDpmrp99mAJHM6tH48Pd1HjRNHWjn6bnh0nXWjwANXX1ZEe+8ggCNg==", + "requires": { + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/icon": "^12.6.0", + "@leafygreen-ui/lib": "^13.6.1", + "@leafygreen-ui/palette": "^4.0.10", + "@leafygreen-ui/polymorphic": "^2.0.0", + "@leafygreen-ui/tokens": "^2.9.0" + } + } + } + }, "@leafygreen-ui/confirmation-modal": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@leafygreen-ui/confirmation-modal/-/confirmation-modal-5.2.0.tgz", @@ -53038,9 +53243,9 @@ } }, "@leafygreen-ui/form-field": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/form-field/-/form-field-1.2.4.tgz", - "integrity": "sha512-cMmeyjsOjEDott5wbdS7pc2EiUa0sNKhTcPlh5DmZy3jDUL1FB5XXvnxlw6xmieZJxtpFBIguaPpToIggLaLrA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/form-field/-/form-field-1.2.5.tgz", + "integrity": "sha512-XH7vJZbgn6wnS7Wv0DpNqcL8q0qPqxHsrVBnqk+iKlnGjCjo1GFzngjOIHODUymEfWRJERrxKO6z8FsSof0GsQ==", "requires": { "@leafygreen-ui/emotion": "^4.0.8", "@leafygreen-ui/hooks": "^8.1.3", @@ -53097,11 +53302,11 @@ } }, "@leafygreen-ui/hooks": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-8.1.3.tgz", - "integrity": "sha512-UAHii7T+g8h8sSzogqUgIid64bbKPHGihAAoBpNzbNsjqFllYVC0FpF59jQeL6tCYd32C2KatWOvhYheBf1hsA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-8.3.0.tgz", + "integrity": "sha512-6z+QdFKB9UniDs9gwwXcWalHAypxEaZG7DOh/o9VSqh+yrfQKm+muPzU/oo40ts72trv1tBVv1pU4+X2oBbzmw==", "requires": { - "@leafygreen-ui/lib": "^13.3.0", + "@leafygreen-ui/lib": "^13.8.1", "lodash": "^4.17.21" } }, @@ -53115,9 +53320,9 @@ } }, "@leafygreen-ui/icon-button": { - "version": "15.0.22", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/icon-button/-/icon-button-15.0.22.tgz", - "integrity": "sha512-o9+gSUfL5ZE6g05m89vv0BRtD+qcfOpfgbuusN5KXdvbAKPgUaweySFl6rMHgSybfdMM1E36rmxyyCwEo7Vahw==", + "version": "15.0.23", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/icon-button/-/icon-button-15.0.23.tgz", + "integrity": "sha512-UnvHugYqr/FFTmbiascoxM0QMuJogP8d3H413ftCQM3jx8F65JbOwSV2X7QWoesKBTzppAfySbw+zMNihP2L1w==", "requires": { "@leafygreen-ui/a11y": "^1.4.13", "@leafygreen-ui/box": "^3.1.9", @@ -53143,14 +53348,14 @@ } }, "@leafygreen-ui/inline-definition": { - "version": "6.0.14", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/inline-definition/-/inline-definition-6.0.14.tgz", - "integrity": "sha512-vCfSF1Lr8O4sm8f7w9rTflVyJRjF3Tyrtppr9OSfEPTDDlla+tiuSyvrMUty3xfdomc6JEGyumdozvjyU9dFsg==", + "version": "6.0.15", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/inline-definition/-/inline-definition-6.0.15.tgz", + "integrity": "sha512-Lv6j68szWlAol8CU+CI2ip3AcxoOvX/tizsWvNg46atyrkQg4cw0ow/PQMFpO0BwzCCEZK0hGXxbjrQmDWfUvg==", "requires": { - "@leafygreen-ui/emotion": "^4.0.7", - "@leafygreen-ui/lib": "^13.0.0", - "@leafygreen-ui/palette": "^4.0.7", - "@leafygreen-ui/tooltip": "^11.0.0" + "@leafygreen-ui/emotion": "^4.0.8", + "@leafygreen-ui/lib": "^13.3.0", + "@leafygreen-ui/palette": "^4.0.9", + "@leafygreen-ui/tooltip": "^11.0.3" } }, "@leafygreen-ui/input-option": { @@ -53177,9 +53382,9 @@ } }, "@leafygreen-ui/lib": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/lib/-/lib-13.7.0.tgz", - "integrity": "sha512-R+2br+QrCABPefv5SD4DOAduIveoVxFtSRqk51frjLyATHLUhg7SwV783KJ0ipofCfsLdae2CZRSzT7MAVbSEA==", + "version": "13.8.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/lib/-/lib-13.8.2.tgz", + "integrity": "sha512-UxtZauF0rsB2dT0dsFYadcs9qa22Wk3PJaSXOCoI8BRPxyV8H4H6B+FQuFjCeLpKWFYOGLee9di3Xsqd4ewa8Q==", "requires": { "@storybook/csf": "^0.1.0", "lodash": "^4.17.21", @@ -55104,6 +55309,7 @@ "@leafygreen-ui/card": "^10.0.6", "@leafygreen-ui/checkbox": "^12.1.1", "@leafygreen-ui/code": "^14.3.1", + "@leafygreen-ui/combobox": "^9.1.6", "@leafygreen-ui/confirmation-modal": "^5.2.0", "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/guide-cue": "^5.0.6", diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json index 510402db94d..3c90191c82b 100644 --- a/packages/compass-components/package.json +++ b/packages/compass-components/package.json @@ -40,6 +40,7 @@ "@leafygreen-ui/card": "^10.0.6", "@leafygreen-ui/checkbox": "^12.1.1", "@leafygreen-ui/code": "^14.3.1", + "@leafygreen-ui/combobox": "^9.1.6", "@leafygreen-ui/confirmation-modal": "^5.2.0", "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/guide-cue": "^5.0.6", @@ -47,9 +48,7 @@ "@leafygreen-ui/icon": "^12.0.0", "@leafygreen-ui/icon-button": "^15.0.20", "@leafygreen-ui/info-sprinkle": "^1.0.3", - "@leafygreen-ui/inline-definition": "^6.0.14", "@leafygreen-ui/leafygreen-provider": "^3.1.12", - "@leafygreen-ui/lib": "^13.2.1", "@leafygreen-ui/logo": "^9.1.1", "@leafygreen-ui/marketing-modal": "^4.2.1", "@leafygreen-ui/menu": "^23.0.2", @@ -84,7 +83,6 @@ "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", "polished": "^4.2.2", - "prop-types": "^15.7.2", "react": "^17.0.2", "react-hotkeys-hook": "^4.3.7", "react-intersection-observer": "^8.34.0", diff --git a/packages/compass-components/src/components/combobox-with-custom-option.tsx b/packages/compass-components/src/components/combobox-with-custom-option.tsx index daebce04712..fa2f1759dc8 100644 --- a/packages/compass-components/src/components/combobox-with-custom-option.tsx +++ b/packages/compass-components/src/components/combobox-with-custom-option.tsx @@ -1,11 +1,13 @@ import React, { useState, useMemo } from 'react'; -import { Combobox } from './combobox'; -import type { - ComboboxProps, - onChangeType, - SelectValueType, -} from './combobox/Combobox.types'; +import { Combobox } from './leafygreen'; +import type { ComboboxProps } from '@leafygreen-ui/combobox'; +type SelectValueType = NonNullable< + ComboboxProps['value'] +>; +type OnChangeType = NonNullable< + ComboboxProps['onChange'] +>; type ComboboxWithCustomOptionProps = ComboboxProps & { options: K[]; renderOption: (option: K, index: number, isCustom: boolean) => JSX.Element; @@ -36,7 +38,7 @@ export const ComboboxWithCustomOption = < ); } return _opts; - }, [userOptions, customOptions, search]); + }, [userOptions, customOptions, search, renderOption]); const selectValueAndRunOnChange = (value: string[] | string | null) => { if (!onChange || !value) return; @@ -47,13 +49,13 @@ export const ComboboxWithCustomOption = < .filter((value) => !userOptions.find((x) => x.value === value)) .map((x) => ({ value: x })) as K[]; setCustomOptions(customOptions); - (onChange as onChangeType)(multiSelectValues); + (onChange as OnChangeType)(multiSelectValues); } else { const selectValue = value as SelectValueType; if (selectValue && !userOptions.find((x) => x.value === selectValue)) { setCustomOptions([{ value: selectValue } as K]); } - (onChange as onChangeType)(selectValue); + (onChange as OnChangeType)(selectValue); } }; diff --git a/packages/compass-components/src/components/combobox-with-custom-options.spec.tsx b/packages/compass-components/src/components/combobox-with-custom-options.spec.tsx index 58487318576..ddc22cb4ab5 100644 --- a/packages/compass-components/src/components/combobox-with-custom-options.spec.tsx +++ b/packages/compass-components/src/components/combobox-with-custom-options.spec.tsx @@ -9,7 +9,7 @@ import { } from '@mongodb-js/testing-library-compass'; import { ComboboxWithCustomOption } from './combobox-with-custom-option'; -import { ComboboxOption } from './combobox/ComboboxOption'; +import { ComboboxOption } from './leafygreen'; const renderCombobox = ( props: Partial> = {} diff --git a/packages/compass-components/src/components/combobox/Chip.tsx b/packages/compass-components/src/components/combobox/Chip.tsx deleted file mode 100644 index 2de83c6cebf..00000000000 --- a/packages/compass-components/src/components/combobox/Chip.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import React, { useContext, useEffect, useMemo, useRef } from 'react'; - -import { css, cx } from '@leafygreen-ui/emotion'; -import Icon from '@leafygreen-ui/icon'; -import InlineDefinition from '@leafygreen-ui/inline-definition'; -import { keyMap, Theme } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import { transitionDuration, typeScales } from '@leafygreen-ui/tokens'; - -import { - chipClassName, - chipWrapperPaddingY, - inputHeight, -} from './Combobox.styles'; -import { ChipProps, ComboboxSize as Size } from './Combobox.types'; -import { ComboboxContext } from './ComboboxContext'; - -const chipWrapperBaseStyle = css` - display: inline-flex; - align-items: center; - overflow: hidden; - white-space: nowrap; - box-sizing: border-box; -`; - -const chipWrapperSizeStyle: Record = { - [Size.Default]: css` - font-size: ${typeScales.body1.fontSize}px; - line-height: ${typeScales.body1.lineHeight}px; - border-radius: 4px; - `, - [Size.Large]: css` - font-size: ${typeScales.body2.fontSize}px; - line-height: ${typeScales.body2.lineHeight}px; - border-radius: 4px; - `, -}; - -const chipWrapperThemeStyle: Record = { - [Theme.Light]: css` - color: ${palette.black}; - background-color: ${palette.gray.light2}; - - // TODO: - refine these styles with Design - &:focus-within { - background-color: ${palette.blue.light2}; - } - `, - [Theme.Dark]: css` - color: ${palette.gray.light2}; - background-color: ${palette.gray.dark2}; - - &:focus-within { - background-color: ${palette.blue.dark2}; - } - `, -}; - -const disabledChipWrapperStyle: Record = { - [Theme.Light]: css` - cursor: not-allowed; - color: ${palette.gray.base}; - background-color: ${palette.gray.light3}; - `, - [Theme.Dark]: css` - cursor: not-allowed; - color: ${palette.gray.dark2}; - background-color: ${palette.gray.dark4}; - box-shadow: inset 0 0 1px 1px ${palette.gray.dark2}; ; - `, -}; - -const chipTextSizeStyle: Record = { - [Size.Default]: css` - padding-inline: 6px; - padding-block: ${chipWrapperPaddingY[Size.Default]}px; - `, - [Size.Large]: css` - padding-inline: 10px; - padding-block: ${chipWrapperPaddingY[Size.Large]}px; - `, -}; - -const chipButtonStyle = css` - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - outline: none; - border: none; - background-color: transparent; - cursor: pointer; - transition: background-color ${transitionDuration.faster}ms ease-in-out; -`; - -const chipButtonSizeStyle: Record = { - [Size.Default]: css` - height: ${inputHeight[Size.Default]}px; - `, - [Size.Large]: css` - height: ${inputHeight[Size.Large]}px; - `, -}; - -const chipButtonThemeStyle: Record = { - [Theme.Light]: css` - color: ${palette.gray.dark2}; - - &:hover { - color: ${palette.black}; - background-color: ${palette.gray.light1}; - } - `, - [Theme.Dark]: css` - color: ${palette.gray.light1}; - - &:hover { - color: ${palette.gray.light3}; - background-color: ${palette.gray.dark1}; - } - `, -}; - -const chipButtonDisabledStyle: Record = { - [Theme.Light]: css` - cursor: not-allowed; - color: ${palette.gray.dark2}; - &:hover { - color: inherit; - background-color: unset; - } - `, - [Theme.Dark]: css` - cursor: not-allowed; - color: ${palette.gray.dark2}; - &:hover { - color: inherit; - background-color: unset; - } - `, -}; - -export const Chip = React.forwardRef( - ({ displayName, isFocused, onRemove, onFocus }: ChipProps, forwardedRef) => { - const { - darkMode, - theme, - size, - disabled, - chipTruncationLocation = 'end', - chipCharacterLimit = 12, - } = useContext(ComboboxContext); - - const isTruncated = - !!chipCharacterLimit && - !!chipTruncationLocation && - chipTruncationLocation !== 'none' && - displayName.length > chipCharacterLimit; - - const buttonRef = useRef(null); - - const truncatedName = useMemo(() => { - if (isTruncated) { - const ellipsis = '…'; - const chars = chipCharacterLimit - 3; // ellipsis dots included in the char limit - - switch (chipTruncationLocation) { - case 'start': { - const end = displayName - .substring(displayName.length - chars) - .trim(); - return ellipsis + end; - } - - case 'middle': { - const start = displayName.substring(0, chars / 2).trim(); - const end = displayName - .substring(displayName.length - chars / 2) - .trim(); - return start + ellipsis + end; - } - - case 'end': { - const start = displayName.substring(0, chars).trim(); - return start + ellipsis; - } - - default: { - return displayName; - } - } - } - - return false; - }, [chipCharacterLimit, chipTruncationLocation, displayName, isTruncated]); - - useEffect(() => { - if (isFocused && !disabled) { - buttonRef?.current?.focus(); - } - }, [disabled, forwardedRef, isFocused]); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if ( - !disabled && - (e.key === keyMap.Delete || - e.key === keyMap.Backspace || - e.key === keyMap.Enter || - e.key === keyMap.Space) - ) { - onRemove(); - } - }; - - const handleChipClick = (e: React.MouseEvent) => { - // Did not click button - if (!buttonRef.current?.contains(e.target as Node)) { - onFocus(); - } - }; - - const handleButtonClick = () => { - if (!disabled) { - onRemove(); - } - }; - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events - - - {truncatedName ? ( - - {truncatedName} - - ) : ( - displayName - )} - - - - ); - } -); -Chip.displayName = 'Chip'; diff --git a/packages/compass-components/src/components/combobox/Combobox.styles.ts b/packages/compass-components/src/components/combobox/Combobox.styles.ts deleted file mode 100644 index c24b99a1b4b..00000000000 --- a/packages/compass-components/src/components/combobox/Combobox.styles.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; -import { createUniqueClassName, Theme } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import { - focusRing, - fontFamilies, - hoverRing, - spacing, - transitionDuration, - typeScales, -} from '@leafygreen-ui/tokens'; - -import { ComboboxSize as Size, Overflow } from './Combobox.types'; - -/** - * Width of the widest character (in px) - */ -export const maxCharWidth: Record = { - [Size.Default]: typeScales.body1.fontSize, - [Size.Large]: typeScales.body2.fontSize, -}; - -/** - * Vertical padding on a chip (in px) - */ -export const chipWrapperPaddingY = { - [Size.Default]: 2, - [Size.Large]: 4, -} as const; - -/** - * Height of the input element (in px) - */ -export const inputHeight: Record = { - [Size.Default]: - typeScales.body1.lineHeight + 2 * chipWrapperPaddingY[Size.Default], // 20 - [Size.Large]: - typeScales.body2.lineHeight + 2 * chipWrapperPaddingY[Size.Large], // 28 -}; - -/** - * Size of combobox x & y padding (in px) - */ -export const comboboxPadding: Record = { - [Size.Default]: { - y: (36 - inputHeight[Size.Default] - 2) / 2, - x: spacing[2] - 1, - }, - [Size.Large]: { - y: (48 - inputHeight[Size.Large] - 2) / 2, - x: spacing[2] - 1, - }, -}; - -/** Width of the clear icon (in px) */ -export const clearButtonIconSize = 28; - -/** Width of the dropdown caret icon (in px) */ -export const caretIconSize = spacing[3]; - -const minWidth: Record = { - [Size.Default]: - maxCharWidth[Size.Default] + - 2 * comboboxPadding[Size.Default].x + - caretIconSize + - 2, // + 2 for border: ; - [Size.Large]: - maxCharWidth[Size.Large] + - 2 * comboboxPadding[Size.Large].x + - caretIconSize + - 2, // + 2 for border: ; -}; - -export const chipClassName = createUniqueClassName('combobox-chip'); - -export const comboboxParentStyle = (size: Size): string => { - return css` - font-family: ${fontFamilies.default}; - width: 100%; - min-width: ${minWidth[size]}px; - `; -}; - -export const baseComboboxStyles = css` - display: grid; - grid-auto-flow: column; - grid-template-columns: 1fr ${caretIconSize}px; - align-items: center; - cursor: text; - transition: ${transitionDuration.default}ms ease-in-out; - transition-property: background-color, box-shadow, border-color; - border: 1px solid; - width: 100%; - max-width: 100%; - border-radius: 6px; -`; - -export const comboboxThemeStyles: Record = { - [Theme.Light]: css` - color: ${palette.gray.dark3}; - background-color: ${palette.white}; - border-color: ${palette.gray.base}; - - &:hover { - box-shadow: ${hoverRing[Theme.Light].gray}; - } - `, - [Theme.Dark]: css` - color: ${palette.gray.light2}; - background-color: ${palette.gray.dark4}; - border-color: ${palette.gray.base}; - - &:hover { - box-shadow: ${hoverRing[Theme.Dark].gray}; - } - `, -}; - -export const comboboxSizeStyles = (size: Size) => css` - padding: ${comboboxPadding[size].y}px ${comboboxPadding[size].x}px; -`; - -export const comboboxDisabledStyles: Record = { - [Theme.Light]: css` - cursor: not-allowed; - color: ${palette.gray.dark1}; - background-color: ${palette.gray.light2}; - border-color: ${palette.gray.light1}; - `, - [Theme.Dark]: css` - cursor: not-allowed; - color: ${palette.gray.dark1}; - background-color: ${palette.gray.dark3}; - border-color: ${palette.gray.dark2}; - `, -}; - -export const comboboxErrorStyles: Record = { - [Theme.Light]: css` - border-color: ${palette.red.base}; - `, - [Theme.Dark]: css` - border-color: ${palette.red.light1}; - `, -}; - -export const comboboxFocusStyle: Record = { - [Theme.Light]: css` - &:focus-within { - border-color: transparent; - background-color: ${palette.white}; - box-shadow: ${focusRing[Theme.Light].input}; - } - `, - [Theme.Dark]: css` - &:focus-within { - border-color: transparent; - background-color: ${palette.gray.dark4}; - box-shadow: ${focusRing[Theme.Dark].input}; - } - `, -}; - -export const comboboxSelectionStyles = css` - grid-template-columns: 1fr ${clearButtonIconSize}px ${caretIconSize}px; -`; - -export const inputWrapperStyle = ({ - overflow, - size, -}: { - overflow: Overflow; - size: Size; -}) => { - const baseWrapperStyle = css` - flex-grow: 1; - width: 100%; - `; - - switch (overflow) { - case Overflow.scrollX: { - return css` - ${baseWrapperStyle} - display: block; - height: ${inputHeight[size]}px; - white-space: nowrap; - overflow-x: scroll; - scroll-behavior: smooth; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - - & > .${chipClassName} { - margin-inline: 2px; - - &:first-child { - margin-inline-start: 0; - } - - &:last-child { - margin-inline-end: 0; - } - } - `; - } - - // TODO - look into animating input element height on wrap - case Overflow.expandY: { - return css` - ${baseWrapperStyle} - display: flex; - flex-wrap: wrap; - gap: 4px; - overflow-x: hidden; - min-height: ${inputHeight[size]}px; - `; - } - } -}; - -export const baseInputElementStyle = css` - font-family: ${fontFamilies.default}; - width: 100%; - border: none; - cursor: inherit; - background-color: inherit; - color: inherit; - box-sizing: content-box; - padding: 0; - margin: 0; - text-overflow: ellipsis; - - &:placeholder-shown { - min-width: 100%; - } - &:focus { - outline: none; - } -`; - -export const inputElementThemeStyle: Record = { - [Theme.Light]: css` - &::placeholder { - color: ${palette.gray.dark1}; - } - `, - [Theme.Dark]: css` - &::placeholder { - color: ${palette.gray.light1}; - } - `, -}; - -export const inputElementSizeStyle: Record = { - [Size.Default]: css` - height: ${inputHeight[Size.Default]}px; - font-size: ${typeScales.body1.fontSize}px; - line-height: ${typeScales.body1.lineHeight}px; - min-width: ${maxCharWidth[Size.Default]}px; - // Only add padding if there are chips - &:not(:first-child) { - padding-left: 4px; - } - `, - [Size.Large]: css` - height: ${inputHeight[Size.Large]}px; - font-size: ${typeScales.body2.fontSize}px; - line-height: ${typeScales.body2.lineHeight}px; - min-width: ${maxCharWidth[Size.Large]}px; - &:not(:first-child) { - padding-left: 6px; - } - `, -}; - -export const inputElementTransitionStyles = (isOpen: boolean) => css` - /* - * Immediate transition in, slow transition out. - * '-in' transition is handled by \`scroll-behavior\` - */ - transition: width ease-in-out ${isOpen ? '0s' : '100ms'}; -`; - -// Previously defined in inputWrapperStyle -/** Should only be applied to a multiselect */ -export const multiselectInputElementStyle = ( - size: Size, - inputValue?: string -) => { - const inputLength = inputValue?.length ?? 0; - return css` - width: ${inputLength * maxCharWidth[size]}px; - max-width: 100%; - `; -}; - -export const clearButtonStyle = css` - // Add a negative margin so the button takes up the same space as the regular icons - margin-block: calc(${caretIconSize / 2}px - 100%); -`; - -export const endIconStyle = (size: Size) => css` - height: ${caretIconSize}px; - width: ${caretIconSize}px; - margin-inline-end: calc(${comboboxPadding[size].x}px / 2); -`; - -export const errorMessageThemeStyle: Record = { - [Theme.Light]: css` - color: ${palette.red.base}; - `, - [Theme.Dark]: css` - color: ${palette.red.light1}; - `, -}; - -export const errorMessageSizeStyle: Record = { - [Size.Default]: css` - font-size: ${typeScales.body1.fontSize}px; - line-height: ${typeScales.body1.lineHeight}px; - padding-top: ${comboboxPadding[Size.Default].y}px; - `, - [Size.Large]: css` - font-size: ${typeScales.body2.fontSize}px; - line-height: ${typeScales.body2.lineHeight}px; - padding-top: ${comboboxPadding[Size.Large].y}px; - `, -}; -export const labelDescriptionContainerStyle = css` - margin-bottom: 2px; -`; diff --git a/packages/compass-components/src/components/combobox/Combobox.tsx b/packages/compass-components/src/components/combobox/Combobox.tsx deleted file mode 100644 index 7f2df9596da..00000000000 --- a/packages/compass-components/src/components/combobox/Combobox.tsx +++ /dev/null @@ -1,1455 +0,0 @@ -/* eslint-disable filename-rules/match */ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import clone from 'lodash/clone'; -import isArray from 'lodash/isArray'; -import isEqual from 'lodash/isEqual'; -import isNull from 'lodash/isNull'; -import isString from 'lodash/isString'; -import isUndefined from 'lodash/isUndefined'; -import PropTypes from 'prop-types'; - -import { css, cx } from '@leafygreen-ui/emotion'; -import { - useDynamicRefs, - useEventListener, - useIdAllocator, - usePrevious, -} from '@leafygreen-ui/hooks'; -import Icon from '@leafygreen-ui/icon'; -import IconButton from '@leafygreen-ui/icon-button'; -import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import { consoleOnce, isComponentType, keyMap } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import { Description, Label } from '@leafygreen-ui/typography'; - -import { ComboboxMenu } from './ComboboxMenu/ComboboxMenu'; -import { Chip } from './Chip'; -import { - baseComboboxStyles, - baseInputElementStyle, - clearButtonStyle, - comboboxDisabledStyles, - comboboxErrorStyles, - comboboxFocusStyle, - comboboxParentStyle, - comboboxSelectionStyles, - comboboxSizeStyles, - comboboxThemeStyles, - endIconStyle, - errorMessageSizeStyle, - errorMessageThemeStyle, - inputElementSizeStyle, - inputElementThemeStyle, - inputElementTransitionStyles, - inputWrapperStyle, - labelDescriptionContainerStyle, - multiselectInputElementStyle, -} from './Combobox.styles'; -import type { - ComboboxProps, - onChangeType, - OptionObject, - SelectValueType, -} from './Combobox.types'; -import { - ComboboxElement, - ComboboxSize, - getNullSelection, - Overflow, - SearchState, - State, - TruncationLocation, -} from './Combobox.types'; -import { ComboboxContext } from './ComboboxContext'; -import { InternalComboboxGroup } from './ComboboxGroup'; -import { InternalComboboxOption } from './ComboboxOption'; -import { - flattenChildren, - getDisplayNameForValue, - getNameAndValue, - getOptionObjectFromValue, - getValueForDisplayName, -} from './utils'; -import { spacing } from '@leafygreen-ui/tokens'; - -const descriptionWidth = spacing[5] * 14; - -// By default we want the menu option to be the same width as the input -// If the user has specified a description, we add extra space to fit the description. -const popoverMenuStyles = (width: number, numDescChars: number) => { - if (numDescChars === 0) { - return css` - width: ${width}px; - `; - } - - const descWithExtraSpace = numDescChars + 5; - - return css` - width: calc( - ${width}px + min(${descriptionWidth}px, ${descWithExtraSpace}ch) - ); - - margin-left: calc( - min(${descriptionWidth / 2}px, ${descWithExtraSpace / 2}ch) - ); - `; -}; - -/** - * Combobox is a combination of a Select and TextInput, - * allowing the user to either type a value directly or select a value from the list. - * Can be configured to select a single or multiple options. - */ -export function Combobox({ - children, - label, - description, - placeholder = 'Select', - 'aria-label': ariaLabel, - disabled = false, - size = ComboboxSize.Default, - darkMode: darkModeProp, - state = 'none', - errorMessage, - searchState = 'unset', - searchEmptyMessage = 'No results found', - searchErrorMessage = 'Could not get results!', - searchLoadingMessage = 'Loading results...', - filteredOptions, - onFilter, - clearable = true, - onClear, - overflow = 'expand-y', - multiselect = false as M, - initialValue, - onChange, - value, - chipTruncationLocation, - chipCharacterLimit = 12, - className, - usePortal = true, - portalClassName, - portalContainer, - scrollContainer, - popoverZIndex, - popoverClassName, - ...rest -}: ComboboxProps) { - const { darkMode, theme } = useDarkMode(darkModeProp); - const getOptionRef = useDynamicRefs({ prefix: 'option' }); - const getChipRef = useDynamicRefs({ prefix: 'chip' }); - - const inputId = useIdAllocator({ prefix: 'combobox-input' }); - const labelId = useIdAllocator({ prefix: 'combobox-label' }); - const menuId = useIdAllocator({ prefix: 'combobox-menu' }); - - const comboboxRef = useRef(null); - const clearButtonRef = useRef(null); - const inputWrapperRef = useRef(null); - const inputRef = useRef(null); - const menuRef = useRef(null); - - const [isOpen, setOpen] = useState(false); - const wasOpen = usePrevious(isOpen); - const [highlightedOption, setHighlightedOption] = useState( - null - ); - const [selection, setSelection] = useState | null>(null); - const prevSelection = usePrevious(selection); - const [inputValue, setInputValue] = useState(''); - const prevValue = usePrevious(inputValue); - const [focusedChip, setFocusedChip] = useState(null); - - const doesSelectionExist = - !isNull(selection) && - ((isArray(selection) && selection.length > 0) || isString(selection)); - - const placeholderValue = - multiselect && isArray(selection) && selection.length > 0 - ? undefined - : placeholder; - - const closeMenu = () => setOpen(false); - const openMenu = () => setOpen(true); - - /** - * Array of all of the options objects - */ - const allOptions: Array = useMemo( - () => flattenChildren(children), - [children] - ); - - /** - * Utility function that tells Typescript whether selection is multiselect - */ - const isMultiselect = useCallback( - (val?: Array | T | null): val is Array => { - if (multiselect && (typeof val === 'string' || typeof val === 'number')) { - consoleOnce.error( - `Error in Combobox: multiselect is set to \`true\`, but received a ${typeof val} value: "${val}"` - ); - } else if (!multiselect && isArray(val)) { - consoleOnce.error( - 'Error in Combobox: multiselect is set to `false`, but received an Array value' - ); - } - - return multiselect && isArray(val); - }, - [multiselect] - ); - - /** - * Forces focus of input box - * @param cursorPos index the cursor should be set to - */ - const setInputFocus = useCallback( - (cursorPos?: number) => { - if (!disabled && inputRef && inputRef.current) { - inputRef.current.focus(); - if (!isUndefined(cursorPos)) { - inputRef.current.setSelectionRange(cursorPos, cursorPos); - } - } - }, - [disabled] - ); - - /** - * Update selection. - * This behaves differently in multi. vs single select. - * @param value option value the selection should be set to - */ - const updateSelection = useCallback( - (value: string | null) => { - if (isMultiselect(selection)) { - // We know M is true here - const newSelection: SelectValueType = clone(selection); - - if (isNull(value)) { - newSelection.length = 0; - } else { - if (selection.includes(value)) { - // remove from array - newSelection.splice(newSelection.indexOf(value), 1); - } else { - // add to array - newSelection.push(value); - // clear text - setInputValue(''); - } - } - setSelection(newSelection as SelectValueType); - (onChange as onChangeType)?.(newSelection); - } else { - const newSelection: SelectValueType = value as SelectValueType; - setSelection(newSelection); - (onChange as onChangeType)?.( - newSelection as SelectValueType - ); - } - }, - [isMultiselect, onChange, selection] - ); - - /** - * Returns whether a given value is included in, or equal to, the current selection - * @param value the option value to check - */ - const isValueCurrentSelection = useCallback( - (value: string): boolean => { - return isMultiselect(selection) - ? selection.includes(value) - : value === selection; - }, - [isMultiselect, selection] - ); - - /** - * Returns whether given text is included in, or equal to, the current selection. - * Similar to `isValueCurrentSelection`, but assumes the text argument is the `displayName` for the selection - * @param text the text to check - */ - const isTextCurrentSelection = useCallback( - (text: string): boolean => { - const value = getValueForDisplayName(text, allOptions); - return isValueCurrentSelection(value); - }, - [allOptions, isValueCurrentSelection] - ); - - /** - * Returns whether the provided option is disabled - * @param option the option value or OptionObject to check - */ - const isOptionDisabled = (option: string | OptionObject): boolean => { - if (typeof option === 'string') { - const optionObj = getOptionObjectFromValue(option, allOptions); - return !!optionObj?.isDisabled; - } else { - return !!option.isDisabled; - } - }; - - /** - * Computes whether the option is visible based on the current input - * @param option the option value or OptionObject to compute - */ - const shouldOptionBeVisible = useCallback( - (option: string | OptionObject): boolean => { - const value = typeof option === 'string' ? option : option.value; - - // If filtered options are provided - if (filteredOptions && filteredOptions.length > 0) { - return filteredOptions.includes(value); - } - - // If the text input value is the current selection - // (or included in the selection) - // then all options should be visible - if (isTextCurrentSelection(inputValue)) { - return true; - } - - // otherwise, we do our own filtering - const displayName = - typeof option === 'string' - ? getDisplayNameForValue(value, allOptions) - : option.displayName; - - const isValueInDisplayName = displayName - .toLowerCase() - .includes(inputValue.toLowerCase()); - - return isValueInDisplayName; - }, - [filteredOptions, isTextCurrentSelection, inputValue, allOptions] - ); - - /** - * The array of visible options objects - */ - const visibleOptions: Array = useMemo( - () => allOptions.filter(shouldOptionBeVisible), - [allOptions, shouldOptionBeVisible] - ); - - /** - * Returns whether the given value is in the options array - * @param value the value to check - */ - const isValueValid = useCallback( - (value: string | null): boolean => { - return value ? !!allOptions.find((opt) => opt.value === value) : false; - }, - [allOptions] - ); - - /** - * Returns the index of a given value in the array of visible (filtered) options - * @param value the option value to get the index of - */ - const getIndexOfValue = useCallback( - (value: string | null): number => { - return visibleOptions - ? visibleOptions.findIndex((option) => option.value === value) - : -1; - }, - [visibleOptions] - ); - - /** - * Returns the option value of a given index in the array of visible (filtered) options - * @param index the option index to get the value of - */ - const getValueAtIndex = useCallback( - (index: number): string | undefined => { - if (visibleOptions && visibleOptions.length >= index) { - const option = visibleOptions[index]; - return option ? option.value : undefined; - } - }, - [visibleOptions] - ); - - /** - * Returns the index of the active chip in the selection array - */ - const getActiveChipIndex = useCallback( - () => - isMultiselect(selection) - ? selection.findIndex((value) => - getChipRef(value)?.current?.contains(document.activeElement) - ) - : -1, - [getChipRef, isMultiselect, selection] - ); - - /** - * - * Focus Management - * - */ - - const [focusedElementName, trackFocusedElement] = useState< - ComboboxElement | undefined - >(); - const isElementFocused = (elementName: ComboboxElement) => - elementName === focusedElementName; - - type Direction = 'next' | 'prev' | 'first' | 'last'; - - /** - * Updates the highlighted menu option based on the provided direction - * @param direction the direction to move the focus. `'next' | 'prev' | 'first' | 'last'` - */ - const updateHighlightedOption = useCallback( - (direction: Direction) => { - const optionsCount = visibleOptions?.length ?? 0; - const lastIndex = optionsCount - 1 > 0 ? optionsCount - 1 : 0; - const indexOfHighlight = getIndexOfValue(highlightedOption); - - // Remove focus from chip - if (direction && isOpen) { - setFocusedChip(null); - setInputFocus(); - } - - switch (direction) { - case 'next': { - const newValue = - indexOfHighlight + 1 < optionsCount - ? getValueAtIndex(indexOfHighlight + 1) - : getValueAtIndex(0); - - setHighlightedOption(newValue ?? null); - break; - } - - case 'prev': { - const newValue = - indexOfHighlight - 1 >= 0 - ? getValueAtIndex(indexOfHighlight - 1) - : getValueAtIndex(lastIndex); - - setHighlightedOption(newValue ?? null); - break; - } - - case 'last': { - const newValue = getValueAtIndex(lastIndex); - setHighlightedOption(newValue ?? null); - break; - } - - case 'first': - default: { - const newValue = getValueAtIndex(0); - setHighlightedOption(newValue ?? null); - } - } - }, - [ - highlightedOption, - getIndexOfValue, - getValueAtIndex, - isOpen, - setInputFocus, - visibleOptions?.length, - ] - ); - - /** - * Updates the focused chip based on the provided direction - * @param direction the direction to move the focus. `'next' | 'prev' | 'first' | 'last'` - * @param relativeToIndex the chip index to move focus relative to - */ - const updateFocusedChip = useCallback( - (direction: Direction | null, relativeToIndex?: number) => { - if (isMultiselect(selection)) { - switch (direction) { - case 'next': { - const referenceChipIndex = relativeToIndex ?? getActiveChipIndex(); - const nextChipIndex = - referenceChipIndex + 1 < selection.length - ? referenceChipIndex + 1 - : selection.length - 1; - const nextChipValue = selection[nextChipIndex]; - setFocusedChip(nextChipValue); - break; - } - - case 'prev': { - const referenceChipIndex = relativeToIndex ?? getActiveChipIndex(); - const prevChipIndex = - referenceChipIndex > 0 - ? referenceChipIndex - 1 - : referenceChipIndex < 0 - ? selection.length - 1 - : 0; - const prevChipValue = selection[prevChipIndex]; - setFocusedChip(prevChipValue); - break; - } - - case 'first': { - const firstChipValue = selection[0]; - setFocusedChip(firstChipValue); - break; - } - - case 'last': { - const lastChipValue = selection[selection.length - 1]; - setFocusedChip(lastChipValue); - break; - } - - default: - setFocusedChip(null); - break; - } - } - }, - [getActiveChipIndex, isMultiselect, selection] - ); - - /** - * Handles an arrow key press - */ - const handleArrowKey = useCallback( - (direction: 'left' | 'right', event: React.KeyboardEvent) => { - // Remove focus from menu - if (direction) setHighlightedOption(null); - - switch (direction) { - case 'right': - switch (focusedElementName) { - case ComboboxElement.Input: { - // If cursor is at the end of the input - if ( - inputRef.current?.selectionEnd === - inputRef.current?.value.length - ) { - clearButtonRef.current?.focus(); - } - break; - } - - case ComboboxElement.FirstChip: - case ComboboxElement.MiddleChip: - case ComboboxElement.LastChip: { - if ( - focusedElementName === ComboboxElement.LastChip || - // the first chip is also the last chip (i.e. only one) - selection?.length === 1 - ) { - // if focus is on last chip, go to input - setInputFocus(0); - updateFocusedChip(null); - event.preventDefault(); - break; - } - // First/middle chips - updateFocusedChip('next'); - break; - } - - case ComboboxElement.ClearButton: - default: - break; - } - break; - - case 'left': - switch (focusedElementName) { - case ComboboxElement.ClearButton: { - event.preventDefault(); - setInputFocus(inputRef?.current?.value.length); - break; - } - - case ComboboxElement.Input: - case ComboboxElement.MiddleChip: - case ComboboxElement.LastChip: { - if (isMultiselect(selection)) { - // Break if cursor is not at the start of the input - if ( - focusedElementName === ComboboxElement.Input && - inputRef.current?.selectionStart !== 0 - ) { - break; - } - - updateFocusedChip('prev'); - } - break; - } - - case ComboboxElement.FirstChip: - default: - break; - } - break; - default: - updateFocusedChip(null); - break; - } - }, - [ - focusedElementName, - isMultiselect, - selection, - setInputFocus, - updateFocusedChip, - ] - ); - - // When the input value changes (or when the menu opens) - // Update the focused option - useEffect(() => { - if (inputValue !== prevValue) { - updateHighlightedOption('first'); - } - }, [inputValue, isOpen, prevValue, updateHighlightedOption]); - - // When the focused option changes, update the menu scroll if necessary - useEffect(() => { - if (highlightedOption) { - const focusedElementRef = getOptionRef(highlightedOption); - - if (focusedElementRef && focusedElementRef.current && menuRef.current) { - const { offsetTop: optionTop } = focusedElementRef.current; - const { scrollTop: menuScroll, offsetHeight: menuHeight } = - menuRef.current; - - if (optionTop > menuHeight || optionTop < menuScroll) { - menuRef.current.scrollTop = optionTop; - } - } - } - }, [highlightedOption, getOptionRef]); - - /** - * Rendering - */ - - /** - * Callback to render a child as an element - */ - const renderOption = useCallback( - (child: React.ReactNode) => { - if (isComponentType(child, 'ComboboxOption')) { - const { value, displayName } = getNameAndValue(child.props); - - if (shouldOptionBeVisible(value)) { - const { className, glyph, disabled } = child.props; - const index = allOptions.findIndex((opt) => opt.value === value); - - const isFocused = highlightedOption === value; - const isSelected = isMultiselect(selection) - ? selection.includes(value) - : selection === value; - - const setSelected = () => { - setHighlightedOption(value); - updateSelection(value); - setInputFocus(); - - if (value === selection) { - closeMenu(); - } - }; - - const optionRef = getOptionRef(value); - - return ( - - ); - } - } else if (isComponentType(child, 'ComboboxGroup')) { - const nestedChildren = React.Children.map( - child.props.children, - renderOption - ); - - if (nestedChildren && nestedChildren?.length > 0) { - return ( - - {React.Children.map(nestedChildren, renderOption)} - - ); - } - } - }, - [ - allOptions, - getOptionRef, - highlightedOption, - isMultiselect, - selection, - setInputFocus, - shouldOptionBeVisible, - updateSelection, - ] - ); - - /** - * The rendered JSX elements for the options - */ - const renderedOptionsJSX = useMemo( - () => React.Children.map(children, renderOption), - [children, renderOption] - ); - - /** - * The rendered JSX for the selection Chips - */ - const renderedChips = useMemo(() => { - if (isMultiselect(selection)) { - return selection.filter(isValueValid).map((value, index) => { - const displayName = getDisplayNameForValue(value, allOptions); - const isFocused = focusedChip === value; - const chipRef = getChipRef(value); - const isLastChip = index >= selection.length - 1; - - const onRemove = () => { - if (isLastChip) { - // Focus the input if this is the last chip in the set - setInputFocus(); - updateFocusedChip(null); - } else { - updateFocusedChip('next', index); - } - updateSelection(value); - }; - - const onFocus = () => { - setFocusedChip(value); - }; - - return ( - - ); - }); - } - }, [ - isMultiselect, - selection, - isValueValid, - allOptions, - focusedChip, - getChipRef, - updateSelection, - setInputFocus, - updateFocusedChip, - ]); - - const handleClearButtonFocus = () => { - setHighlightedOption(null); - }; - - /** - * The rendered JSX for the input icons (clear, warn & caret) - */ - const renderedInputIcons = useMemo(() => { - const handleClearButtonClick = ( - e: React.MouseEvent - ) => { - if (!disabled) { - updateSelection(null); - onClear?.(e); - onFilter?.(''); - if (!isOpen) { - openMenu(); - } - } - }; - - return ( - <> - {clearable && doesSelectionExist && ( - - - - )} - {state === 'error' ? ( - - ) : ( - - )} - - ); - }, [ - clearable, - doesSelectionExist, - disabled, - state, - darkMode, - size, - updateSelection, - onClear, - onFilter, - isOpen, - ]); - - /** - * Flag to determine whether the rendered options have icons - */ - const withIcons = useMemo( - () => allOptions.some((opt) => opt.hasGlyph), - [allOptions] - ); - - /** - * - * Selection Management - * - */ - - const onCloseMenu = useCallback(() => { - // Single select, and no change to selection - if (!isMultiselect(selection) && selection === prevSelection) { - const exactMatchedOption = visibleOptions.find( - (option) => - option.displayName === inputValue || option.value === inputValue - ); - - // check if inputValue is matches a valid option - // Set the selection to that value if the component is not controlled - if (exactMatchedOption && !value) { - setSelection(exactMatchedOption.value as SelectValueType); - } else if (selection) { - // Revert the value to the previous selection. - // Set the value instead of displayName to align with handleInputChange COMPASS-6511 - setInputValue(selection); - } - } - }, [ - allOptions, - inputValue, - isMultiselect, - prevSelection, - selection, - value, - visibleOptions, - ]); - - const onSelect = useCallback(() => { - if (doesSelectionExist) { - if (isMultiselect(selection)) { - // Scroll the wrapper to the end. No effect if not `overflow="scroll-x"` - scrollInputToEnd(); - } else if (!isMultiselect(selection)) { - // Update the text input. - // Set the value instead of displayName to align with handleInputChange COMPASS-6511 - setInputValue(selection); - closeMenu(); - } - } else { - setInputValue(''); - } - }, [doesSelectionExist, allOptions, isMultiselect, selection]); - - // Set the initialValue - useEffect(() => { - if (initialValue) { - if (isArray(initialValue)) { - // Ensure the values we set are real options - const filteredValue = - initialValue.filter((value) => isValueValid(value)) ?? []; - setSelection(filteredValue as SelectValueType); - } else { - if (isValueValid(initialValue)) { - setSelection(initialValue); - } - } - } else { - setSelection(getNullSelection(multiselect)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // When controlled value changes, update the selection - useEffect(() => { - if (!isUndefined(value) && value !== prevValue) { - if (isNull(value)) { - setSelection(null); - } else if (isMultiselect(value)) { - // Ensure the value(s) passed in are valid options - const newSelection = value.filter(isValueValid) as SelectValueType; - setSelection(newSelection); - } else { - setSelection( - isValueValid(value as SelectValueType) ? value : null - ); - } - } - }, [isMultiselect, isValueValid, prevValue, value]); - - // onSelect - // Side effects to run when the selection changes - useEffect(() => { - if (!isEqual(selection, prevSelection)) { - onSelect(); - } - }, [onSelect, prevSelection, selection]); - - // when the menu closes, update the value if needed - useEffect(() => { - if (!isOpen && wasOpen) { - onCloseMenu(); - } - }, [isOpen, wasOpen, onCloseMenu]); - - /** - * - * Menu management - * - */ - - const [popoverMenuWidth, setPopoverMenuWidth] = useState(0); - - // When the menu opens, or the selection changes, or the focused option changes - // update the menu width - useEffect(() => { - setPopoverMenuWidth(comboboxRef.current?.clientWidth ?? 0); - }, [comboboxRef, isOpen, highlightedOption, selection]); - - // Handler fired when the menu has finished transitioning in/out - const handleTransitionEnd = () => { - setPopoverMenuWidth(comboboxRef.current?.clientWidth ?? 0); - }; - - /** - * - * Event Handlers - * - */ - - // Prevent combobox from gaining focus by default - const handleInputWrapperMousedown = (e: React.MouseEvent) => { - if (disabled) { - e.preventDefault(); - } - }; - - // Set focus to the input element on click - const handleComboboxClick = (e: React.MouseEvent) => { - // If we clicked the wrapper, not the input itself. - // (Focus is set automatically if the click is on the input) - if (e.target !== inputRef.current) { - let cursorPos = 0; - - if (inputRef.current) { - const mouseX = e.nativeEvent.offsetX; - const inputRight = - inputRef.current.offsetLeft + inputRef.current.clientWidth; - cursorPos = mouseX > inputRight ? inputValue.length : 0; - } - - setInputFocus(cursorPos); - } - - // Only open the menu in response to a click - openMenu(); - }; - - // Fired whenever the wrapper gains focus, - // and any time the focus within changes - const handleComboboxFocus = (e: React.FocusEvent) => { - scrollInputToEnd(); - trackFocusedElement(getNameFromElement(e.target)); - }; - - // Fired onChange - const handleInputChange = ({ - target: { value }, - }: React.ChangeEvent) => { - setInputValue(value); - // fire any filter function passed in - onFilter?.(value); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - const isFocusInMenu = menuRef.current?.contains(document.activeElement); - const isFocusOnCombobox = comboboxRef.current?.contains( - document.activeElement - ); - - const isFocusInComponent = isFocusOnCombobox || isFocusInMenu; - - // Only run if the focus is in the component - if (isFocusInComponent) { - // No support for modifiers yet - // TODO - Handle support for multiple chip selection - if (event.ctrlKey || event.shiftKey || event.altKey) { - return; - } - - switch (event.key) { - case keyMap.Tab: { - switch (focusedElementName) { - case 'Input': { - if (!doesSelectionExist) { - closeMenu(); - updateHighlightedOption('first'); - updateFocusedChip(null); - } - // else use default behavior - break; - } - - case 'LastChip': { - // use default behavior - updateFocusedChip(null); - break; - } - - case 'FirstChip': - case 'MiddleChip': { - // use default behavior - break; - } - - case 'ClearButton': - default: - break; - } - - break; - } - - case keyMap.Escape: { - closeMenu(); - updateHighlightedOption('first'); - break; - } - - case keyMap.Enter: { - if (!isOpen) { - // If the menu is not open, enter should open the menu - openMenu(); - } else if ( - // Select the highlighted option iff - // the menu is open, - // we're focused on input element, - // and the highlighted option is not disabled - focusedElementName === ComboboxElement.Input && - !isNull(highlightedOption) && - !isOptionDisabled(highlightedOption) - ) { - updateSelection(highlightedOption); - } else if ( - // Focused on clear button - focusedElementName === ComboboxElement.ClearButton - ) { - updateSelection(null); - setInputFocus(); - } - break; - } - - case keyMap.Backspace: { - // Backspace key focuses last chip if the input is focused - // Note: Chip removal behavior is handled in `onRemove` defined in `renderChips` - if (isMultiselect(selection)) { - if ( - focusedElementName === 'Input' && - inputRef.current?.selectionStart === 0 - ) { - updateFocusedChip('last'); - } - } - // Open the menu regardless - openMenu(); - break; - } - - case keyMap.ArrowDown: { - if (isOpen) { - // Prevent the page from scrolling - event.preventDefault(); - // only change option if the menu is already open - updateHighlightedOption('next'); - } else { - openMenu(); - } - break; - } - - case keyMap.ArrowUp: { - if (isOpen) { - // Prevent the page from scrolling - event.preventDefault(); - // only change option if the menu is already open - updateHighlightedOption('prev'); - } else { - openMenu(); - } - break; - } - - case keyMap.ArrowRight: { - handleArrowKey('right', event); - break; - } - - case keyMap.ArrowLeft: { - handleArrowKey('left', event); - break; - } - - default: { - if (!isOpen) { - openMenu(); - } - } - } - } - }; - - /** - * - * Global Event Handler - * - */ - - /** - * We add two event handlers to the document to handle the backdrop click behavior. - * Intended behavior is to close the menu, and keep focus on the Combobox. - * No other click event handlers should fire on backdrop click - * - * 1. Mousedown event fires - * 2. We prevent `mousedown`'s default behavior, to prevent focus from being applied to the body (or other target) - * 3. Click event fires - * 4. We handle this event on _capture_, and stop propagation before the `click` event propagates all the way to any other element. - * This ensures that even if we click on a button, that handler is not fired - * 5. Then we call `closeMenu`, setting `isOpen = false`, and rerender the component - */ - useEventListener( - 'mousedown', - (mousedown: MouseEvent) => { - if (!doesComponentContainEventTarget(mousedown)) { - mousedown.preventDefault(); // Prevent focus from being applied to body - mousedown.stopPropagation(); // Stop any other mousedown events from firing - } - }, - { - enabled: isOpen, - } - ); - useEventListener( - 'click', - (click: MouseEvent) => { - if (!doesComponentContainEventTarget(click)) { - click.stopPropagation(); // Stop any other click events from firing - closeMenu(); - } - }, - { - options: { capture: true }, - enabled: isOpen, - } - ); - - const popoverProps = { - popoverZIndex, - ...(usePortal - ? { - usePortal, - portalClassName, - portalContainer, - scrollContainer, - } - : { usePortal }), - } as const; - - const descriptionCharacters = useMemo(() => { - const characters: number[] = - React.Children.map( - children, - (child: any) => child?.props?.description?.length ?? 0 - ) ?? []; - return characters.length === 0 ? 0 : Math.max(...characters); - }, [children]); - - return ( - -
- {(label || description) && ( -
- {label && ( - - )} - {description && ( - {description} - )} -
- )} - - {/* Disable eslint: onClick sets focus. Key events would already have focus */} - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} -
-
- {renderedChips} - -
- {renderedInputIcons} -
- - {state === 'error' && errorMessage && ( -
- {errorMessage} -
- )} - - {/******* / - * Menu * - / *******/} - - - {renderedOptionsJSX} - -
-
- ); - - // Closure-dependant utils - - /** - * Returns whether the event target is a Combobox element - */ - function doesComponentContainEventTarget({ target }: MouseEvent): boolean { - return ( - menuRef.current?.contains(target as Node) || - comboboxRef.current?.contains(target as Node) || - false - ); - } - - /** - * Scrolls the combobox to the far right. - * Used when `overflow === 'scroll-x'`. - * Has no effect otherwise - */ - function scrollInputToEnd() { - if (inputWrapperRef && inputWrapperRef.current) { - // TODO - consider converting to .scrollTo(). This is not yet supported in IE or jsdom - inputWrapperRef.current.scrollLeft = inputWrapperRef.current.scrollWidth; - } - } - - /** - * Returns the provided element as a ComboboxElement string - */ - function getNameFromElement( - element?: Element | null - ): ComboboxElement | undefined { - if (!element) return; - if (inputRef.current?.contains(element)) return ComboboxElement.Input; - if (clearButtonRef.current?.contains(element)) - return ComboboxElement.ClearButton; - - const activeChipIndex = isMultiselect(selection) - ? selection.findIndex((value) => - getChipRef(value)?.current?.contains(element) - ) - : -1; - - if (isMultiselect(selection)) { - if (activeChipIndex === 0) return ComboboxElement.FirstChip; - if (activeChipIndex === selection.length - 1) - return ComboboxElement.LastChip; - if (activeChipIndex > 0) return ComboboxElement.MiddleChip; - } - - if (menuRef.current?.contains(element)) return ComboboxElement.Menu; - if (comboboxRef.current?.contains(element)) return ComboboxElement.Combobox; - } -} - -Combobox.propTypes = { - // Multiselect props - multiselect: PropTypes.bool, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - initialValue: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - overflow: PropTypes.oneOf(Object.values(Overflow)), - - // Standard Props - darkMode: PropTypes.bool, - label: PropTypes.string, - 'aria-label': PropTypes.string, - children: PropTypes.node, - onChange: PropTypes.func, - chipCharacterLimit: PropTypes.number, - chipTruncationLocation: PropTypes.oneOf(Object.values(TruncationLocation)), - onClear: PropTypes.func, - onFilter: PropTypes.func, - clearable: PropTypes.bool, - searchLoadingMessage: PropTypes.string, - searchErrorMessage: PropTypes.string, - searchEmptyMessage: PropTypes.string, - searchState: PropTypes.oneOf(Object.values(SearchState)), - errorMessage: PropTypes.string, - state: PropTypes.oneOf(Object.values(State)), - size: PropTypes.oneOf(Object.values(ComboboxSize)), - disabled: PropTypes.bool, - description: PropTypes.string, - placeholder: PropTypes.string, - filteredOptions: PropTypes.arrayOf(PropTypes.string), - // Popover Props - popoverZIndex: PropTypes.number, - usePortal: PropTypes.bool, - scrollContainer: PropTypes.elementType, - portalContainer: PropTypes.elementType, - portalClassName: PropTypes.string, -}; - -/** - * Why'd you have to go and make things so complicated? - * - Avril; and also me to myself about this component - */ diff --git a/packages/compass-components/src/components/combobox/Combobox.types.ts b/packages/compass-components/src/components/combobox/Combobox.types.ts deleted file mode 100644 index 7063425d6e6..00000000000 --- a/packages/compass-components/src/components/combobox/Combobox.types.ts +++ /dev/null @@ -1,378 +0,0 @@ -/* eslint-disable filename-rules/match */ - -import type { ReactElement, ReactNode } from 'react'; -import type { Either, HTMLElementProps } from '@leafygreen-ui/lib'; - -/** - * Prop Enums & Types - */ - -export const ComboboxElement = { - Input: 'Input', - ClearButton: 'ClearButton', - FirstChip: 'FirstChip', - LastChip: 'LastChip', - MiddleChip: 'MiddleChip', - Combobox: 'Combobox', - Menu: 'Menu', -} as const; -export type ComboboxElement = - typeof ComboboxElement[keyof typeof ComboboxElement]; - -/** - * Prop types - */ - -export const ComboboxSize = { - // TODO: add XSmall & Small variants after the refresh - // XSmall: 'xsmall', - // Small: 'small', - Default: 'default', - Large: 'large', -} as const; -export type ComboboxSize = typeof ComboboxSize[keyof typeof ComboboxSize]; - -export const TruncationLocation = { - start: 'start', - middle: 'middle', - end: 'end', - none: 'none', -} as const; -export type TruncationLocation = - typeof TruncationLocation[keyof typeof TruncationLocation]; - -export const Overflow = { - /** - * Combobox will be set to a fixed width, and will expand its height based on the number of Chips selected - */ - expandY: 'expand-y', - /** - * Combobox will be set to a fixed height and width (default 100% of container). Chips will be scrollable left-right - */ - scrollX: 'scroll-x', - /** - * @deprecated - */ - expandX: 'expand-x', -} as const; -export type Overflow = typeof Overflow[keyof typeof Overflow]; - -export const State = { - error: 'error', - none: 'none', -} as const; -export type State = typeof State[keyof typeof State]; - -export const SearchState = { - unset: 'unset', - error: 'error', - loading: 'loading', -} as const; -export type SearchState = typeof SearchState[keyof typeof SearchState]; - -/** - * Generic Typing - */ - -export type SelectValueType = M extends true - ? Array - : string | null; - -export type onChangeType = M extends true - ? (value: SelectValueType) => void - : (value: SelectValueType) => void; - -// Returns the correct empty state for multiselcect / single select -export function getNullSelection( - multiselect: M -): SelectValueType { - if (multiselect) { - return [] as Array as SelectValueType; - } else { - return null as SelectValueType; - } -} - -/** - * Combobox Props - */ - -export interface ComboboxMultiselectProps { - /** - * Defines whether a user can select multiple options, or only a single option. - * When using TypeScript, `multiselect` affects the valid values of `initialValue`, `value`, and `onChange` - */ - multiselect?: M; - /** - * The initial selection. - * Must be a string (or array of strings) that matches the `value` prop of a `ComboboxOption`. - * Changing the `initialValue` after initial render will not change the selection. - */ - initialValue?: SelectValueType; - /** - * A callback called when the selection changes. - * Callback receives a single argument that is the new selection, either string, or string array - */ - onChange?: onChangeType; - /** - * The controlled value of the Combobox. - * Must be a string (or array of strings) that matches the `value` prop of a `ComboboxOption`. - * Changing `value` after initial render _will_ affect the selection. - * `value` will always take precedence over `initialValue` if both are provided. - */ - value?: SelectValueType; - - /** - * Defines the overflow behavior of a multiselect combobox. - * - * `expand-y`: Combobox has fixed width, and additional selections will cause the element to grow in the block direction. - * - * `expand-x`: Combobox has fixed height, and additional selections will cause the element to grow in the inline direction. - * - * `scroll-x`: Combobox has fixed height and width, and additional selections will cause the element to be scrollable in the x (horizontal) direction. - */ - overflow?: M extends true ? Overflow : undefined; -} - -export interface BaseComboboxProps - extends Omit, 'onChange'> { - /** - * Defines the Combobox Options by passing children. Must be `ComboboxOption` or `ComboboxGroup` - */ - children?: ReactNode; - - /** - * An accessible label for the input, rendered in a