diff --git a/.vscode/settings.json b/.vscode/settings.json index 247f63a..638ebfa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,8 @@ "consts", "@reduxjs", "testid", - "inited" + "inited", + "headlessui" ], "cSpell.words": [ "Nothingg" diff --git a/extractedTranslations/en/translation.json b/extractedTranslations/en/translation.json index b12a581..35852cd 100644 --- a/extractedTranslations/en/translation.json +++ b/extractedTranslations/en/translation.json @@ -105,6 +105,7 @@ "search-filter": "search-filter", "seartch": "seartch", "select-currency": "", + "select-value": "", "send-new-comment": "send-new-comment", "sort-by": "sort-by", "sort-direction": "sort-direction", diff --git a/package-lock.json b/package-lock.json index 8f880ba..f726ba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@headlessui/react": "^1.7.17", "@reduxjs/toolkit": "^1.9.5", "axios": "^1.4.0", "babel-plugin-i18next-extract": "^0.9.0", @@ -3066,6 +3067,21 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@headlessui/react": { + "version": "1.7.17", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz", + "integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==", + "dependencies": { + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -9840,6 +9856,11 @@ "node": ">=8" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -25237,6 +25258,14 @@ "@hapi/hoek": "^9.0.0" } }, + "@headlessui/react": { + "version": "1.7.17", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz", + "integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==", + "requires": { + "client-only": "^0.0.1" + } + }, "@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -30102,6 +30131,11 @@ } } }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", diff --git a/package.json b/package.json index 6dc92a1..452021b 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "webpack-dev-server": "^4.7.4" }, "dependencies": { + "@headlessui/react": "^1.7.17", "@reduxjs/toolkit": "^1.9.5", "axios": "^1.4.0", "babel-plugin-i18next-extract": "^0.9.0", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 201de2f..e031710 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -14,5 +14,6 @@ "desc-sort": "descending", "sort-field-creation-time": "creation time", "sort-filed-title": "title", - "sort-field-views": "views" + "sort-field-views": "views", + "select-value": "Choose a value" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index eb50fa0..c38888b 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -14,5 +14,6 @@ "desc-sort": "убыванию", "sort-field-creation-time": "дате создания", "sort-filed-title": "заголовку", - "sort-field-views": "просмотрам" + "sort-field-views": "просмотрам", + "select-value": "Выберите значение" } diff --git a/src/app/App.tsx b/src/app/App.tsx index e544a5b..a128e8b 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,6 +1,5 @@ import { FC, Suspense, useEffect } from 'react'; import { classNames } from 'shared/lib/classNames/classNames'; -import { HStack } from 'shared/ui/Stack'; import { useAppDispatch } from 'shared/lib/hooks/useAppDispatch'; import { Navbar } from 'widgets/Navbar'; import { Aside } from 'widgets/Aside'; @@ -20,10 +19,10 @@ const App: FC = () => {
- +
); diff --git a/src/app/styles/themes/blue.scss b/src/app/styles/themes/blue.scss index 4dbe8be..1f6a7a3 100644 --- a/src/app/styles/themes/blue.scss +++ b/src/app/styles/themes/blue.scss @@ -16,4 +16,7 @@ // * Card bg --card-bg: #101557; + + // * ListBox bg + --list-box-bg: #101557; } \ No newline at end of file diff --git a/src/app/styles/themes/dark.scss b/src/app/styles/themes/dark.scss index 54c891f..1cff837 100644 --- a/src/app/styles/themes/dark.scss +++ b/src/app/styles/themes/dark.scss @@ -16,4 +16,7 @@ // * Card bg --card-bg: #202074; + + // * ListBox bg + --list-box-bg: #202074; } \ No newline at end of file diff --git a/src/app/styles/themes/light.scss b/src/app/styles/themes/light.scss index f62a56d..9c2f9bd 100644 --- a/src/app/styles/themes/light.scss +++ b/src/app/styles/themes/light.scss @@ -16,4 +16,7 @@ // * Card bg --card-bg: #d3d3d3; + + // * ListBox bg + --list-box-bg: #d3d3d3; } \ No newline at end of file diff --git a/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.module.scss b/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.module.scss index 033546d..91a535a 100644 --- a/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.module.scss +++ b/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.module.scss @@ -1,7 +1,5 @@ .ArticlesListFilters { - display: flex; - flex-direction: column; - gap: 16px; + // padding: 12px; } .sort-filters { diff --git a/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.tsx b/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.tsx index 6f2d5fe..4fb8fb7 100644 --- a/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.tsx +++ b/src/features/ArticlesList/ui/ArticlesListFilters/ArticlesListFilters.tsx @@ -20,6 +20,7 @@ import { getArticlesListView, } from '../../model/selectors/articlesList'; import cls from './ArticlesListFilters.module.scss'; +import { VStack } from 'shared/ui/Stack'; interface ArticlesListFiltersProps { className?: string; @@ -77,7 +78,7 @@ export const ArticlesListFilters: FC = memo(({ classNa ], []); return ( -
+
= memo(({ classNa onTabClick={onChangeTag} value={tag} /> -
+
); }); diff --git a/src/pages/ArticlesPage/ui/ArticlesPage/ArticlesPage.tsx b/src/pages/ArticlesPage/ui/ArticlesPage/ArticlesPage.tsx index ab77527..d68c594 100644 --- a/src/pages/ArticlesPage/ui/ArticlesPage/ArticlesPage.tsx +++ b/src/pages/ArticlesPage/ui/ArticlesPage/ArticlesPage.tsx @@ -11,7 +11,10 @@ import { getArticlesListView, initArticlesList, ArticlesList, + ArticlesListFilters, } from 'features/ArticlesList'; +import { VStack } from 'shared/ui/Stack'; +import { PageWrapper } from 'widgets/PageWrapper'; interface ArticlesPageProps { className?: string; @@ -23,6 +26,7 @@ const reducers: ReducersList = { const ArticlesPage: FC = ({ className }) => { const dispatch = useAppDispatch(); + const isVirtualized = false; const articles = useAppSelector(getArticlesList.selectAll); const isLoading = useAppSelector(getArticlesListIsLoading); @@ -39,14 +43,32 @@ const ArticlesPage: FC = ({ className }) => { return ( - + {isVirtualized ? ( + + ) : ( + + + + + + + )} ); }; diff --git a/src/shared/ui/ListBox/ListBox.module.scss b/src/shared/ui/ListBox/ListBox.module.scss new file mode 100644 index 0000000..9f9663d --- /dev/null +++ b/src/shared/ui/ListBox/ListBox.module.scss @@ -0,0 +1,37 @@ +.ListBox { + position: relative; +} + +.trigger { + background: none; + margin: 0; + padding: 0; + outline: none; + border: none; +} + +.options { + position: absolute; + background: var(--list-box-bg); + border-radius: 0 0 12px 12px; + z-index: 20; + width: 100%; + max-width: fit-content; + + &:active { + outline: none; + } +} + +.option { + padding: 10px 20px; + cursor: pointer; +} + +.active { + background: var(--inverted-primary-color); +} + +.disabled { + opacity: .5; +} \ No newline at end of file diff --git a/src/shared/ui/ListBox/ListBox.stories.tsx b/src/shared/ui/ListBox/ListBox.stories.tsx new file mode 100644 index 0000000..d36583a --- /dev/null +++ b/src/shared/ui/ListBox/ListBox.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ListBox } from './ListBox'; + +const meta: Meta = { + title: 'shared/ListBox', + component: ListBox, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Root: Story = { + args: { + options: [ + { + content: 'First option label', + value: 'First option value', + }, + { + content: 'Second option label', + value: 'Second option value', + }, + { + content: 'Third option label', + value: 'Third option value', + }, + ], + }, +}; + +export const WithDisabledOption: Story = { + args: { + options: [ + { + content: 'First label', + value: 'First value', + }, + { + content: 'Second label', + value: 'Second value', + disabled: true, + }, + { + content: 'Third label', + value: 'Third value', + }, + ], + }, +}; diff --git a/src/shared/ui/ListBox/ListBox.tsx b/src/shared/ui/ListBox/ListBox.tsx new file mode 100644 index 0000000..37c2526 --- /dev/null +++ b/src/shared/ui/ListBox/ListBox.tsx @@ -0,0 +1,68 @@ +import { Fragment, ReactElement, ReactNode, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Listbox } from '@headlessui/react'; +import { classNames, Mods } from 'shared/lib/classNames/classNames'; +import { Button } from '../Button'; +import cls from './ListBox.module.scss'; + +export interface ListBoxOption { + content: ReactNode; + value: T; + disabled?: boolean; +} + +interface ListBoxProps { + className?: string; + options: ListBoxOption[]; + value?: T; + onChange?: (value: T) => void; + readonly?: boolean; +} + +export const ListBox = (props: ListBoxProps): ReactElement> => { + const { t } = useTranslation('translation'); + const { className, options, value, onChange, readonly } = props; + + const defaultLabel = options.find((o) => o.value === value) || { content: t('select-value'), value: '' as T }; + const [selectedValue, setSelectedValue] = useState>(defaultLabel); + + const onChangeHandler = useCallback((option: ListBoxOption) => { + setSelectedValue(option); + onChange?.(option.value); + }, [onChange]); + + return ( + + + + + + {options.map((option, index) => ( + + {({ active, selected, disabled }) => { + const mods: Mods = { + [cls.active]: active, + [cls.selected]: selected, + [cls.disabled]: disabled, + }; + + return ( +
  • + {option.content} +
  • + ); + }} +
    + ))} +
    +
    + ); +}; diff --git a/src/shared/ui/ListBox/index.ts b/src/shared/ui/ListBox/index.ts new file mode 100644 index 0000000..5b6769c --- /dev/null +++ b/src/shared/ui/ListBox/index.ts @@ -0,0 +1 @@ +export { ListBox } from './ListBox';