Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
display: flex;
flex-shrink: 0;
flex-direction: column;
gap: 48px;
box-sizing: border-box;
width: 616px;
height: auto;
Expand Down
131 changes: 127 additions & 4 deletions src/components/article-params-form/ArticleParamsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,137 @@
import { FormEvent, useEffect, useRef, useState } from 'react';
import clsx from 'clsx';
import { ArrowButton } from 'src/ui/arrow-button';
import { Button } from 'src/ui/button';
import { RadioGroup } from 'src/ui/radio-group';
import { Select } from 'src/ui/select';
import { Separator } from 'src/ui/separator';
import { Text } from 'src/ui/text';

import {
OptionType,
fontFamilyOptions,
fontColors,
backgroundColors,
contentWidthArr,
fontSizeOptions,
ArticleStateType,
} from 'src/constants/articleProps';

import styles from './ArticleParamsForm.module.scss';

export const ArticleParamsForm = () => {
type ArticleParamsFormProps = {
isOpen: boolean;
onToggle: () => void;
initialState: ArticleStateType;
currentState: ArticleStateType;
onApply?: (values: ArticleStateType) => void;
onReset?: (values: ArticleStateType) => void;
onClose?: () => void;
};

export const ArticleParamsForm = ({
isOpen,
onToggle,
initialState,
currentState,
onApply,
onReset,
onClose,
}: ArticleParamsFormProps) => {
const [formState, setFormState] = useState<ArticleStateType>(currentState);
const containerRef = useRef<HTMLElement | null>(null);

useEffect(() => {
setFormState(currentState);
}, [currentState]);

useEffect(() => {
if (!isOpen) {
return;
}

const handleClickOutside = (event: MouseEvent) => {
const root = containerRef.current;
const target = event.target as Node | null;
if (root && target && !root.contains(target)) {
onClose?.();
}
};

window.addEventListener('mousedown', handleClickOutside);

return () => {
window.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);

const handleChange =
<Key extends keyof ArticleStateType>(key: Key) =>
(option: OptionType) => {
setFormState((prev) => ({
...prev,
[key]: option,
}));
};

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onApply?.(formState);
};

const handleReset = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setFormState(initialState);
onReset?.(initialState);
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно лучше

Можно создать хук useCloseOnOutsideClickOrEsc, который будет закрывать форму при клике вне её и по нажатию клавиши Escape.

Например:

import { useEffect } from 'react';

type UseCloseOnOutsideClickOrEsc = {
	isOpenElement: boolean; // Флаг, открыт ли элемент (например, модальное окно или форма)
	onClose?: () => void;   // Колбэк, вызываемый при закрытии
	elementRef: React.RefObject<HTMLElement>; // Ссылка на DOM-элемент, вне которого отслеживаем клик
};

export const useCloseOnOutsideClickOrEsc = ({
	isOpenElement,
	elementRef,
	onClose,
}: UseCloseOnOutsideClickOrEsc) => {
	useEffect(() => {
		if (!isOpenElement) {
			// Если элемент закрыт, обработчики не нужны
			return;
		}

		const handleClick = ({target}: MouseEvent) => {
			// Если клик был вне элемента — вызываем onClose
			if (
				target instanceof Node &&
				!elementRef.current?.contains(event.target)
			) {
				onClose?.();
			}
		};

		const handleKeyDown = (event: KeyboardEvent) => {
			// Закрытие по нажатию Escape
			if (event.key === 'Escape') {
				onClose?.();
			}
		};

		// Добавляем обработчики
		window.addEventListener('mousedown', handleClick);
		window.addEventListener('keydown', handleKeyDown);

		// Убираем обработчики при размонтировании или изменении зависимостей
		return () => {
			window.removeEventListener('mousedown', handleClick);
			window.removeEventListener('keydown', handleKeyDown);
		};
	}, [isOpenElement, elementRef, onClose]);
};

return (
<>
<ArrowButton isOpen={false} onClick={() => {}} />
<aside className={styles.container}>
<form className={styles.form}>
<ArrowButton isOpen={isOpen} onClick={onToggle} />
<aside
className={clsx(styles.container, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Отлично

То что вы используете библиотеку clsx — это отличное решение для управления условными CSS-классами в React. Это позволяет легко комбинировать классы на основе условий, делает код более читаемым и поддерживаемым, а также обеспечивает удобный способ работы с динамическими стилями компонентов.

[styles.container_open]: isOpen,
})}
ref={containerRef}>
<form
className={styles.form}
onSubmit={handleSubmit}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Отлично

ИспользованиеonSubmit для формы — это правильный подход к обработке отправки формы в React. Это решение перехватывает все способы отправки (клик по кнопке, нажатие Enter в полях ввода), работает с встроенной HTML5-валидацией браузера, позволяет использовать event.preventDefault() для предотвращения перезагрузки страницы, является семантически корректным и следует лучшим практикам веб-разработки, а также обеспечивает лучшую доступность для пользователей вспомогательных технологий.

onReset={handleReset}>
<Text as='h2' size={31} weight={800} uppercase>
Задайте параметры
</Text>
<Select
title='Шрифт'
options={fontFamilyOptions}
selected={formState.fontFamilyOption}
onChange={handleChange('fontFamilyOption')}
/>
<RadioGroup
title='Размер шрифта'
name='font-size'
options={fontSizeOptions}
selected={formState.fontSizeOption}
onChange={handleChange('fontSizeOption')}
/>
<Select
title='Цвет текста'
options={fontColors}
selected={formState.fontColor}
onChange={handleChange('fontColor')}
/>
<Separator />
<Select
title='Цвет фона'
options={backgroundColors}
selected={formState.backgroundColor}
onChange={handleChange('backgroundColor')}
/>
<Select
title='Ширина контента'
options={contentWidthArr}
selected={formState.contentWidth}
onChange={handleChange('contentWidth')}
/>
<div className={styles.bottomContainer}>
<Button title='Сбросить' htmlType='reset' type='clear' />
<Button title='Применить' htmlType='submit' type='apply' />
Expand Down
49 changes: 41 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { createRoot } from 'react-dom/client';
import { StrictMode, CSSProperties } from 'react';
import { CSSProperties, StrictMode, useState } from 'react';
import clsx from 'clsx';

import { Article } from './components/article/Article';
import { ArticleParamsForm } from './components/article-params-form/ArticleParamsForm';
import { defaultArticleState } from './constants/articleProps';
import {
ArticleStateType,
defaultArticleState,
} from './constants/articleProps';

import './styles/index.scss';
import styles from './styles/index.module.scss';
Expand All @@ -13,19 +16,49 @@ const domNode = document.getElementById('root') as HTMLDivElement;
const root = createRoot(domNode);

const App = () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно лучше

Будет лучше вынести компонент App в отдельный файл. Когда каждый компонент находится в отдельном файле, легче ориентироваться в структуре проекта, находить нужный код и вносить изменения. Файл index.js должен служить точкой входа в приложение и отвечать только за рендеринг корневого компонента в DOM.

const [isPanelOpen, setIsPanelOpen] = useState(false);
const [articleState, setArticleState] =
useState<ArticleStateType>(defaultArticleState);

const handleTogglePanel = () => {
setIsPanelOpen((prev) => !prev);
};

const handleClosePanel = () => {
setIsPanelOpen(false);
};

const handleApplyParams = (values: ArticleStateType) => {
setArticleState({ ...values });
handleClosePanel();
};

const handleResetParams = (values: ArticleStateType) => {
setArticleState({ ...values });
handleClosePanel();
};

return (
<main
className={clsx(styles.main)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно лучше

Когда применяется только один CSS-класс без условий, можно не использование clsx , так как библиотека предназначена для управления условными классами. Если класс статичен и не зависит от условий, достаточно использовать обычную строку: className="my-class". Использование clsx в таких случаях добавляет ненужную зависимость и усложняет код без практической пользы.

style={
{
'--font-family': defaultArticleState.fontFamilyOption.value,
'--font-size': defaultArticleState.fontSizeOption.value,
'--font-color': defaultArticleState.fontColor.value,
'--container-width': defaultArticleState.contentWidth.value,
'--bg-color': defaultArticleState.backgroundColor.value,
'--font-family': articleState.fontFamilyOption.value,
'--font-size': articleState.fontSizeOption.value,
'--font-color': articleState.fontColor.value,
'--container-width': articleState.contentWidth.value,
'--bg-color': articleState.backgroundColor.value,
} as CSSProperties
}>
<ArticleParamsForm />
<ArticleParamsForm
isOpen={isPanelOpen}
onToggle={handleTogglePanel}
initialState={defaultArticleState}
currentState={articleState}
onApply={handleApplyParams}
onReset={handleResetParams}
onClose={handleClosePanel}
/>
<Article />
</main>
);
Expand Down
Loading