From 0dc3dc0f420f4f15b81108eba49fc817fd57db09 Mon Sep 17 00:00:00 2001 From: Farhan Ahmad Nurzi <125247833+farhan-nurzi-deriv@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:36:16 +0800 Subject: [PATCH] farhan/feat: Dropdown Component (#10723) * feat: created dropdown component * chore: remove icon right padding, cleaning styles * chore: add transition to icon * refactor: styles * chore: default selected item prop * chore: make onselect required * chore: changes dropdown styles * chore: change types create a util --- package-lock.json | 90 ++++++++++-- packages/wallets/package.json | 7 +- .../Base/WalletDropdown/WalletDropdown.scss | 129 ++++++++++++++++++ .../Base/WalletDropdown/WalletDropdown.tsx | 121 ++++++++++++++++ .../components/Base/WalletDropdown/index.ts | 1 + packages/wallets/src/components/Base/index.ts | 1 + .../modules/Transactions/Transactions.tsx | 15 +- .../TransactionsFilter.scss | 4 - .../TransactionsFilter/TransactionsFilter.tsx | 22 --- .../components/TransactionsFilter/index.ts | 1 - .../modules/Transactions/components/index.ts | 1 - packages/wallets/src/public/images/filter.svg | 5 + .../public/images/pointed-down-arrow-icon.svg | 3 + .../wallets/src/utils/reactNodeToString.ts | 19 +++ 14 files changed, 372 insertions(+), 47 deletions(-) create mode 100644 packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.scss create mode 100644 packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.tsx create mode 100644 packages/wallets/src/components/Base/WalletDropdown/index.ts delete mode 100644 packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.scss delete mode 100644 packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.tsx delete mode 100644 packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/index.ts create mode 100644 packages/wallets/src/public/images/filter.svg create mode 100644 packages/wallets/src/public/images/pointed-down-arrow-icon.svg create mode 100644 packages/wallets/src/utils/reactNodeToString.ts diff --git a/package-lock.json b/package-lock.json index 3e2e7eb1b9aa..d77946bf761e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "css-minimizer-webpack-plugin": "^3.0.1", "deep-diff": "^1.0.2", "dotenv": "^8.2.0", + "downshift": "^8.2.2", "embla-carousel-react": "^8.0.0-rc12", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-binary": "^1.0.2", @@ -1953,11 +1954,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", - "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -1976,6 +1977,11 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/@babel/standalone": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.22.20.tgz", @@ -20346,6 +20352,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "node_modules/concat-map": { "version": "0.0.1", "license": "MIT" @@ -23429,6 +23440,26 @@ "webpack": "^4 || ^5" } }, + "node_modules/downshift": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-8.2.2.tgz", + "integrity": "sha512-UmJHlNTzmFN3i427Hh9f1OXMnkhgSB/J+urC9ywabvwuftm0nB0/Utsb89OtDq+2UqyScQV4Ro7EM2PEV80N5w==", + "dependencies": { + "@babel/runtime": "^7.22.15", + "compute-scroll-into-view": "^3.0.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/downshift/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/duplexer": { "version": "0.1.2", "license": "MIT" @@ -45915,9 +45946,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -50162,11 +50193,18 @@ } }, "@babel/runtime": { - "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", - "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + } } }, "@babel/runtime-corejs3": { @@ -63571,6 +63609,11 @@ } } }, + "compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "concat-map": { "version": "0.0.1" }, @@ -65844,6 +65887,25 @@ "dotenv-defaults": "^2.0.2" } }, + "downshift": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-8.2.2.tgz", + "integrity": "sha512-UmJHlNTzmFN3i427Hh9f1OXMnkhgSB/J+urC9ywabvwuftm0nB0/Utsb89OtDq+2UqyScQV4Ro7EM2PEV80N5w==", + "requires": { + "@babel/runtime": "^7.22.15", + "compute-scroll-into-view": "^3.0.3", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "duplexer": { "version": "0.1.2" }, @@ -81458,9 +81520,9 @@ } }, "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 02a015c38966..443b37c65eb3 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -16,6 +16,7 @@ "@deriv/utils": "^1.0.0", "@tanstack/react-table": "^8.10.3", "classnames": "^2.2.6", + "downshift": "^8.2.2", "embla-carousel-react": "^8.0.0-rc12", "formik": "^2.1.4", "moment": "^2.29.2", @@ -28,6 +29,7 @@ "usehooks-ts": "^2.7.0" }, "devDependencies": { + "@types/css-modules": "^1.0.2", "@types/react-dom": "^18.0.0", "@typescript-eslint/eslint-plugin": "5.45.0", "@typescript-eslint/parser": "5.45.0", @@ -38,9 +40,8 @@ "eslint-plugin-sort-destructure-keys": "^1.5.0", "eslint-plugin-typescript-sort-keys": "^2.3.0", "typescript": "^4.6.3", - "@types/css-modules": "^1.0.2", "webpack": "^5.81.0", - "webpack-cli": "^4.7.2", - "webpack-bundle-analyzer": "^4.3.0" + "webpack-bundle-analyzer": "^4.3.0", + "webpack-cli": "^4.7.2" } } diff --git a/packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.scss b/packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.scss new file mode 100644 index 000000000000..f232d06f24e8 --- /dev/null +++ b/packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.scss @@ -0,0 +1,129 @@ +.wallets-dropdown { + min-width: 12rem; + max-width: 28rem; + position: relative; + + &__button { + all: unset; + margin: auto; + transform: rotate(0); + transform-origin: 50% 45%; + transition: transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1); + + &--active { + transform: rotate(180deg); + } + } + + &__content { + height: 4rem; + width: 100%; + border-radius: 0.4rem; + padding: 1rem 1.6rem; + border: 0.1rem solid var(--system-light-5-active-background, #d6dadb); + background: var(--system-light-8-primary-background, #fff); + display: flex; + gap: 0.8rem; + align-items: center; + } + + &__content:has(&__field:focus) { + border: 0.1rem solid var(--brand-blue, #85acb0); + } + + &__field { + font-family: inherit; + outline: 0; + font-size: 1.4rem; + color: var(--system-light-2-general-text, #333); + transition: border-color 0.2s; + } + + &__field::placeholder { + color: transparent; + } + + &__field:placeholder-shown ~ &__label { + font-size: 1.4rem; + cursor: text; + top: 30%; + padding: 0; + } + + &__field:placeholder-shown ~ &__label--with-icon { + left: 4.4rem; + } + + label, + &__field:focus ~ &__label { + position: absolute; + top: -10%; + display: block; + transition: 0.2s; + font-size: 1rem; + color: var(--system-light-3-less-prominent-text, #999); + background: var(--system-light-8-primary-background, #fff); + padding-inline: 0.4rem; + left: 1.6rem; + } + + &__field:focus ~ &__label { + color: var(--brand-blue, #85acb0); + } + + &__items { + position: absolute; + top: 100%; + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + z-index: 2; + margin-top: 0.4rem; + border-radius: 0.4rem; + background: var(--system-light-8-primary-background, #fff); + box-shadow: 0 3.2rem 6.4rem 0 rgba(14, 14, 14, 0.14); + overflow-y: auto; + + & > :first-child { + border-radius: 0.4rem 0.4rem 0 0; + } + + & > :last-child { + border-radius: 0 0 0.4rem 0.4rem; + } + + &--sm { + max-height: 22rem; + } + + &--md { + max-height: 42rem; + } + + &--lg { + max-height: 66rem; + } + } + + &__icon { + width: 1.6rem; + height: 1.6rem; + margin-left: 0.4rem; + } + + &__item { + padding: 10px 16px; + width: 100%; + z-index: 2; + + &:hover:not(&--active) { + cursor: pointer; + background: var(--system-light-6-hover-background, #e6e9e9); + } + + &--active { + background: var(--system-light-5-active-background, #d6dadb); + } + } +} diff --git a/packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.tsx b/packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.tsx new file mode 100644 index 000000000000..ade6d9ad248d --- /dev/null +++ b/packages/wallets/src/components/Base/WalletDropdown/WalletDropdown.tsx @@ -0,0 +1,121 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import classNames from 'classnames'; +import { useCombobox } from 'downshift'; +import ArrowIcon from '../../../public/images/pointed-down-arrow-icon.svg'; +import reactNodeToString from '../../../utils/reactNodeToString'; +import { WalletText } from '../WalletText'; +import './WalletDropdown.scss'; + +type TProps = { + icon?: React.ReactNode; + label?: React.ReactNode; + list: { + text: React.ReactNode; + value: string; + }[]; + listHeight?: 'lg' | 'md' | 'sm'; + onSelect: (value: string) => void; + type?: 'comboBox' | 'prompt'; + value: string | undefined; +}; + +const WalletDropdown: React.FC = ({ + icon, + label, + list, + listHeight = 'md', + onSelect, + type = 'prompt', + value, +}) => { + const [items, setItems] = useState(list); + const [shouldFilterList, setShouldFilterList] = useState(false); + const clearFilter = useCallback(() => { + setShouldFilterList(false); + setItems(list); + }, [list]); + const { getInputProps, getItemProps, getLabelProps, getMenuProps, getToggleButtonProps, isOpen } = useCombobox({ + items, + itemToString(item) { + return item ? reactNodeToString(item.text) : ''; + }, + onInputValueChange({ inputValue }) { + if (shouldFilterList) { + setItems( + list.filter(item => + reactNodeToString(item.text) + .toLowerCase() + .includes(inputValue?.toLowerCase() ?? '') + ) + ); + } + }, + onIsOpenChange({ isOpen }) { + if (!isOpen) { + clearFilter(); + } + }, + onSelectedItemChange({ selectedItem }) { + onSelect(selectedItem?.value ?? ''); + }, + selectedItem: items.find(item => item.value === value), + }); + + useEffect(() => { + setItems(list); + }, [list]); + + return ( +
+
+ {icon &&
{icon}
} + setShouldFilterList(true)} + placeholder={reactNodeToString(label)} + readOnly={type !== 'comboBox'} + type='text' + value={value} + {...getInputProps()} + /> + + +
+ +
+ ); +}; + +export default WalletDropdown; diff --git a/packages/wallets/src/components/Base/WalletDropdown/index.ts b/packages/wallets/src/components/Base/WalletDropdown/index.ts new file mode 100644 index 000000000000..722c0d14170e --- /dev/null +++ b/packages/wallets/src/components/Base/WalletDropdown/index.ts @@ -0,0 +1 @@ +export { default as WalletDropdown } from './WalletDropdown'; diff --git a/packages/wallets/src/components/Base/index.ts b/packages/wallets/src/components/Base/index.ts index 5392844700e8..32262d2576b6 100644 --- a/packages/wallets/src/components/Base/index.ts +++ b/packages/wallets/src/components/Base/index.ts @@ -7,5 +7,6 @@ export * from './Tabs'; export * from './WalletAlertMessage'; export * from './WalletButton'; export * from './WalletClipboard'; +export * from './WalletDropdown'; export * from './WalletText'; export * from './WalletTextField'; diff --git a/packages/wallets/src/features/cashier/modules/Transactions/Transactions.tsx b/packages/wallets/src/features/cashier/modules/Transactions/Transactions.tsx index c6e6c4709ea5..f21c2412384e 100644 --- a/packages/wallets/src/features/cashier/modules/Transactions/Transactions.tsx +++ b/packages/wallets/src/features/cashier/modules/Transactions/Transactions.tsx @@ -1,6 +1,8 @@ import React, { ComponentProps, useEffect, useState } from 'react'; import { useActiveWalletAccount } from '@deriv/api'; -import { TransactionsCompleted, TransactionsFilter, TransactionsPending } from './components'; +import { WalletDropdown } from '../../../../components'; +import FilterIcon from '../../../../public/images/filter.svg'; +import { TransactionsCompleted, TransactionsPending } from './components'; import './Transactions.scss'; type TTransactionsPendingFilter = ComponentProps['filter']; @@ -59,7 +61,16 @@ const Transactions = () => { )} - + } + label='Filter' + list={Object.keys(filtersMapper[isPendingActive ? 'pending' : 'completed']).map(key => ({ + text: key.replace(/^\w/, c => c.toUpperCase()), + value: key, + }))} + onSelect={value => setFilterValue(value)} + value={filterValue} + /> {isPendingActive ? ( diff --git a/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.scss b/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.scss deleted file mode 100644 index 6016ea8ac539..000000000000 --- a/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.scss +++ /dev/null @@ -1,4 +0,0 @@ -.wallets-transactions-filter { - display: flex; - justify-content: flex-end; -} diff --git a/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.tsx b/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.tsx deleted file mode 100644 index e5b249cedda7..000000000000 --- a/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/TransactionsFilter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import './TransactionsFilter.scss'; - -type TProps = { - isPendingActive: boolean; - onSelect: (value: string) => void; -}; - -const TransactionsFilter: React.FC = ({ isPendingActive, onSelect }) => { - return ( -
- -
- ); -}; - -export default TransactionsFilter; diff --git a/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/index.ts b/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/index.ts deleted file mode 100644 index e0993223a831..000000000000 --- a/packages/wallets/src/features/cashier/modules/Transactions/components/TransactionsFilter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as TransactionsFilter } from './TransactionsFilter'; diff --git a/packages/wallets/src/features/cashier/modules/Transactions/components/index.ts b/packages/wallets/src/features/cashier/modules/Transactions/components/index.ts index eaecfec4d141..3e8a8a928160 100644 --- a/packages/wallets/src/features/cashier/modules/Transactions/components/index.ts +++ b/packages/wallets/src/features/cashier/modules/Transactions/components/index.ts @@ -1,5 +1,4 @@ export * from './TransactionsCompleted'; -export * from './TransactionsFilter'; export * from './TransactionsNoDataState'; export * from './TransactionsPending'; export * from './TransactionsPendingRow'; diff --git a/packages/wallets/src/public/images/filter.svg b/packages/wallets/src/public/images/filter.svg new file mode 100644 index 000000000000..6f29a30de7e7 --- /dev/null +++ b/packages/wallets/src/public/images/filter.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/wallets/src/public/images/pointed-down-arrow-icon.svg b/packages/wallets/src/public/images/pointed-down-arrow-icon.svg new file mode 100644 index 000000000000..3387ffae1513 --- /dev/null +++ b/packages/wallets/src/public/images/pointed-down-arrow-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/wallets/src/utils/reactNodeToString.ts b/packages/wallets/src/utils/reactNodeToString.ts new file mode 100644 index 000000000000..34b2a8aeef72 --- /dev/null +++ b/packages/wallets/src/utils/reactNodeToString.ts @@ -0,0 +1,19 @@ +import React, { isValidElement } from 'react'; + +const reactNodeToString = function (reactNode: React.ReactNode): string { + let string = ''; + if (typeof reactNode === 'string') { + string = reactNode; + } else if (typeof reactNode === 'number') { + string = reactNode.toString(); + } else if (reactNode instanceof Array) { + reactNode.forEach(function (child) { + string += reactNodeToString(child); + }); + } else if (isValidElement(reactNode)) { + string += reactNodeToString(reactNode.props.children); + } + return string; +}; + +export default reactNodeToString;