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';