Не срабатывает анимация одного из трех модальных окон в React

Сделал три модалки с похожей логикой, но не могу понять почему у двух ModalPrice и ModalProduct анимация при закрытии и открытии работает, а у ModalReview не работает. Не один день уже потратил - ChatGTP не видит видимых проблем, в консоли и терминале ошибок нет. Буду очень признателен за помощь.

//ModalPrice.js:
import React, { useEffect, useCallback, useState } from 'react';
import icons from './icons.svg';
import './styles.css';

const ModalPrice = ({ imgSrc, isOpen, onRequestClose }) => {
  const [isClosing, setIsClosing] = useState(false);

  const closeModal = useCallback(() => {
    setIsClosing(true);
    setTimeout(() => onRequestClose(), 500);
  }, [onRequestClose]);

  useEffect(() => {
    const handleEscape = e => isOpen && e.key === 'Escape' && closeModal();

    document.body.classList.toggle('modal-open', isOpen);
    document.body.style.overflow = isOpen ? 'hidden' : 'auto';
    document.addEventListener('keydown', handleEscape);

    return () => {
      document.removeEventListener('keydown', handleEscape);
      document.body.style.overflow = 'auto';
      setIsClosing(false);
    };
  }, [isOpen, closeModal]);

  const overlayClassName = isOpen ? 'modal_overlay active' : 'modal_overlay';
  const modalClassName = isClosing
    ? 'modal_price closing'
    : isOpen
    ? 'modal_price active'
    : 'modal_price';

  return (
    <div
      className={`${overlayClassName}`}
      id="modalOverlay"
      onClick={closeModal}
    >
      <div className={`${modalClassName}`} onClick={e => e.stopPropagation()}>

        <button onClick={closeModal} className="close_modal">
          <svg className="icon_modal">
            <use xlinkHref={`${icons}#close`} />
          </svg>
        </button>
        <div className="image_container">
          <img src={imgSrc} alt="ПРАЙС - ЛИСТ" className="modal_image" />
        </div>
      </div>
    </div>
  );
};

export default ModalPrice;
//ModalProduct.js:
 import React, { useEffect, useState, useCallback } from 'react';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
import icons from './icons.svg';
import './styles.css';

const PrevArrow = ({ onClick }) => (
  <div className="slide_arrow prev" onClick={onClick}>

    <svg className="icon_modal">
      <use xlinkHref={`${icons}#left`} />
    </svg>
  </div>
);

const NextArrow = ({ onClick }) => (
  <div className="slide_arrow next" onClick={onClick}>

    <svg className="icon_modal">
      <use xlinkHref={`${icons}#right`} />
    </svg>
  </div>
);

const ModalProduct = ({ images, closeModal }) => {
  const [modalVisible, setModalVisible] = useState(false);
  const [isClosing, setIsClosing] = useState(false);

  const closeAnimation = useCallback(() => {
    setIsClosing(true);
    setTimeout(() => {
      setModalVisible(false);
      closeModal();
    }, 500);
  }, [closeModal]);

  useEffect(() => {
    const closeOnEsc = event => {
      if (event.key === 'Escape') {
        closeAnimation();
      }
    };

    document.body.addEventListener('keydown', closeOnEsc);
    document.body.style.overflow = 'hidden';

    return () => {
      document.body.removeEventListener('keydown', closeOnEsc);
      document.body.style.overflow = 'auto';
    };
  }, [closeAnimation]);

  const settings = {
    dots: true,
    speed: 1500,
    slidesToShow: 1,
    slidesToScroll: 1,
    autoplay: true,
    autoplaySpeed: 3000,
    infinite: true,
    pauseOnHover: false,
    prevArrow: <PrevArrow />,
    nextArrow: <NextArrow />,
  };

  useEffect(() => {
    setModalVisible(true);
  }, []);

  const overlayClassName = modalVisible
    ? 'modal_overlay active'
    : 'modal_overlay';
  const modalClassName = isClosing
    ? 'modal_product closing'
    : modalVisible
    ? 'modal_product active'
    : 'modal_product';

  return (
    <div className={`${overlayClassName}`} onClick={closeAnimation}>
      <div className={`${modalClassName}`} onClick={e => e.stopPropagation()}>

        <button type="button" className="close_modal" onClick={closeAnimation}>
          <svg className="icon_modal">
            <use xlinkHref={`${icons}#close`} />
          </svg>
        </button>
        <Slider {...settings}>
          {images.map((imageData, index) => (
            <div key={index} className="slide">
              <img
                className="slide_img"
                src={imageData.imageSrc}
                alt={`Slide ${index + 1}`}
              />
              <p
                style={{
                  textAlign: 'center',
                  marginTop: '10px',
                  width: '100%',
                }}
              >
                {imageData.text.includes('(') ? (
                  <>
                    {imageData.text.substring(0, imageData.text.indexOf('('))}
                    <span style={{ fontWeight: 'bold', color: 'red' }}>
                      {imageData.text.substring(imageData.text.indexOf('('))}
                    </span>
                  </>
                ) : (
                  imageData.text
                )}
              </p>
            </div>
          ))}
        </Slider>
      </div>
    </div>
  );
};

export default ModalProduct;
// ModalReview.js:
import React, { useState, useEffect, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { setReviews } from '../../redux/actions';
import { addReviewApi, fetchReviewsApi } from '../../api/api';
import Notiflix from 'notiflix';
import icons from './icons.svg';
import './styles.css';

const ModalReview = ({ closeModal, onSuccess }) => {
  const [newReview, setNewReview] = useState({ name: '', comment: '' });
  const [formErrors, setFormErrors] = useState({});
  const dispatch = useDispatch();
  const [isClosing, setIsClosing] = useState(false);

  useEffect(() => {
    document.body.style.overflow = isClosing ? 'auto' : 'hidden';
  }, [isClosing]);

  const closeModalWithAnimation = useCallback(() => {
    setIsClosing(true);
    setTimeout(() => {
      setIsClosing(false);
      closeModal();
    }, 500);
  }, [closeModal]);

  const handleEscape = useCallback(
    e => {
      if (e.key === 'Escape') {
        closeModalWithAnimation();
      }
    },
    [closeModalWithAnimation]
  );

  useEffect(() => {
    document.addEventListener('keydown', handleEscape);

    return () => {
      document.removeEventListener('keydown', handleEscape);
    };
  }, [handleEscape]);

  const validateForm = () => {
    const errors = {};
    if (!newReview.name.trim()) {
      errors.name = 'Не введено имя.';
    }
    if (!newReview.comment.trim()) {
      errors.comment = 'Не введен комментарий.';
    }
    return errors;
  };

  const handleInputChange = e => {
    const { name, value } = e.target;
    setNewReview({ ...newReview, [name]: value });
  };

  const handleSubmit = async e => {
    e.preventDefault();

    const errors = validateForm();
    setFormErrors(errors);
    if (Object.keys(errors).length === 0) {
      try {
        await addReviewApi(newReview);
        const reviews = await fetchReviewsApi();
        dispatch(setReviews(reviews));
        onSuccess();
        closeModalWithAnimation();
      } catch (error) {
        console.error('Ошибка при добавлении отзыва:', error);
      }
    } else {
      Notiflix.Notify.failure('Будь ласка, заповніть усі поля форми.', {
        position: 'center-center',
        timeout: 2000,
      });
    }
  };

  return (
    <div
      className={`modale_overlay ${isClosing ? 'closing' : ''} ${
        isClosing ? '' : 'active'
      }`}
      onClick={closeModalWithAnimation}
    >
      <div
        className={`modale_content ${isClosing ? 'closing' : ''} ${
          isClosing ? '' : 'active'
        }`}
        onClick={e => e.stopPropagation()}
      >

        <button className="close_modal" onClick={closeModalWithAnimation}>
          <svg className="icon_modal">
            <use xlinkHref={`${icons}#close`} />
          </svg>
        </button>
        <form className="reviews_form" onSubmit={handleSubmit}>
          <label className="reviews_label">
            Ім'я:
            <input
              className="reviews_input"
              type="text"
              name="name"
              value={newReview.name}
              autoComplete="name"
              onChange={handleInputChange}
            />
            {formErrors.name && (
              <div style={{ display: 'none' }}>{formErrors.name}</div>
            )}
          </label>
          <br />
          <label>
            Коментар:
            <textarea
              className="reviews_textarea"
              name="comment"
              value={newReview.comment}
              autoComplete="comment"
              onChange={handleInputChange}
            />
            {formErrors.comment && (
              <div style={{ display: 'none' }}>{formErrors.comment}</div>
            )}
          </label>
          <br />
          <button className="reviews_button modale" type="submit">
            ВІДПРАВИТИ ВІДГУК
          </button>
        </form>
      </div>
    </div>
  );
};

export default ModalReview;

и общий файл со стилями для этих модалок:

/* !-------------------Modal All-------------------------- */

.modal_overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--color-overlay-body);
  z-index: 2000;
  opacity: 0;
  visibility: hidden;
}

.modal_overlay.active {
  opacity: 1;
  visibility: visible;
}

.image_container {
  overflow-y: auto;
  max-height: calc(100vh - 150px);
  margin-top: 10px;
}

.modal_image {
  width: 100%;
  height: auto;
  object-fit: contain;
}

.close_modal {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  justify-content: center;
  align-items: center;
  text-decoration: none;
  border-radius: 50%;
  border: 1px solid var(--color-backdrop);
  width: 30px;
  height: 30px;
  cursor: pointer;
  padding: 0;
  margin: 0;
  color: var(--color-general);
  background-color: var(--background-general);
  transition: color var(--transition-timing) var(--transition-duration);
}

.close_modal:hover,
.close_modal:focus {
  color: var(--color-nav-link);
}

.icon_modal {
  width: 20px;
  height: 20px;
  fill: currentColor;
  align-items: center;
}

/* !-----------------Modal Price-------------------------- */

.modal_price {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 95%;
  height: 100%;
  max-width: 820px;
  max-height: 680px;
  background-color: var(--background-slogan);
  border-radius: 8px;
  padding: 40px;
  margin: 0;
  z-index: 3000;
  transform: translate(-50%, -50%) scale(0);
  opacity: 0;
  transition: opacity var(--transition-duration-product),
    transform var(--transition-duration-product);
}

.modal_price.active {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
}

.modal_price.closing {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0);
  transition: opacity var(--transition-duration-product),
    transform var(--transition-duration-product);
}

/* !---------------Modal Product-------------------------- */

.modal_product {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  max-width: 550px;
  max-height: 630px;
  background-color: var(--background-slogan);
  border-radius: 8px;
  padding: 40px;
  margin: 0;
  z-index: 1000;
  transform: translate(-50%, -50%) scale(0);
  opacity: 0;
  transition: transform var(--transition-timing) var(--transition-duration-product),
    opacity var(--transition-timing) var(--transition-duration-product);
}

.modal_product.active {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
}

.modal_product.closing {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0);
  transition: opacity var(--transition-duration-product),
    transform var(--transition-duration-product);
}

@media screen and (max-width: 1200px) {
  .modal_product {
    max-width: 450px;
    max-height: 580px;
  }
}

/* !---------------Slider Product-------------------------- */

.slide {
  margin-top: 20px;
}

.slide_img {
  width: 100%;
  height: 100%;
  margin: 0 auto;
  max-width: 400px;
  max-height: 500px;
  border-radius: 8px;
  object-fit: contain;
  object-position: center;
}

.slide_arrow {
  position: absolute;
  top: 260px;
  width: 40px;
  height: 40px;
  background-color: var(--background-general);
  color: var(--color-general);
  display: flex;
  justify-content: center;
  align-items: center;
  border: 1px solid var(--color-backdrop);
  border-radius: 50%;
  cursor: pointer;
  transition: color var(--transition-timing) var(--transition-duration);
  z-index: 1;
}

.slide_arrow.prev {
  left: 5px;
}

.slide_arrow.next {
  right: 5px;
}

.slide_arrow:hover,
.slide_arrow:focus {
  color: var(--color-nav-link);
}

/* !---------------Modal Review-------------------------- */

.modale_overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--color-overlay-body);
  z-index: 2000;
  opacity: 0;
  visibility: hidden;
}

.modale_overlay.active {
  opacity: 1;
  visibility: visible;
}

.modale_content {
  position: fixed;
  top: 50%;
  left: 50%;
  background-color: var(--background-slogan);
  padding: 40px;
  padding-top: 20px;
  border-radius: 8px;
  z-index: 1051;
  transform: translate(-50%, -50%) scale(0);
  opacity: 0;
  visibility: hidden;
  transition: transform 500ms, opacity 500ms;
}

.modale_content.active {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
  visibility: visible;
  transition-delay: 0ms;
}

.modale_content.closing {
  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, -50%) scale(0);
  transition: transform 500ms, opacity 500ms;
}

Есть подозрение на эту строку. Как только навешивается класс .closing, прозрачность становится 0, и как следствие анимации не видно.

Не думаю, что эта строка, т.к. стили очень похожи для всех трех модалок, но не работает только в одной

Тогда следующая строка с visibility: hidden;. Это то что отличает стили модалок.

Я этот вариант тоже пробовал, но ничего не помогает. перебрал стили и теперь они такие:

/* !---------------Modal Review-------------------------- */

.modale_overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(–color-overlay-body);
z-index: 2000;
opacity: 0;
visibility: hidden;
}

.modale_overlay.active {
opacity: 1;
visibility: visible;
}

.modale_content {
position: fixed;
top: 50%;
left: 50%;
background-color: var(–background-slogan);
padding: 40px;
padding-top: 20px;
border-radius: 8px;
z-index: 1051;
opacity: 0;
transition: transform var(–transition-timing) var(–transition-duration-product),
opacity var(–transition-timing) var(–transition-duration-product);
}

.modale_content.active {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}

.modale_content.closing {
opacity: 0;
transform: translate(-50%, -50%) scale(0);
transition: opacity var(–transition-duration-product),
transform var(–transition-duration-product);
}

@media screen and (max-width: 768px) {
.modale_content {
padding: 20px;
}
} убрал всё лишнее,но не могу понять чего не хватает или что-то лишнее

Есть еще одна техника отладки подобных ситуаций.

Попробуй запустить debugger когда классы уже присвоены, и анимация должна бы начаться, но еще не должна бы закончиться. Нужна контрукция типа setTimeout(() => {debugger}, 0), поиграйся с таймаутами в 16 и 64 миллисекундами (что соответствует 1 и 4 фреймам).

И посмотри на Computed, фактически примененные стили к элементам. Это покажет перекрывают ли какие стили (и от каких селекторов) те стили которые ты ожидаешь увидеть на элементе. И самое главное увидишь те селекторы которые влияют на конечный примененный стиль.

подправил немного код и стили:

import React, { useState, useEffect, useCallback } from ‘react’;
import { useDispatch } from ‘react-redux’;
import { setReviews } from ‘…/…/redux/actions’;
import { addReviewApi, fetchReviewsApi } from ‘…/…/api/api’;
import Notiflix from ‘notiflix’;
import icons from ‘./icons.svg’;
import ‘./styles.css’;

const ModalReview = ({ isOpen, onRequestClose, onSuccess }) => {
const [newReview, setNewReview] = useState({ name: ‘’, comment: ‘’ });
const [formErrors, setFormErrors] = useState({});
const dispatch = useDispatch();
const [isClosing, setIsClosing] = useState(false);

useEffect(() => {
document.body.style.overflow = isOpen && !isClosing ? ‘hidden’ : ‘auto’;
}, [isOpen, isClosing]);

const closeModalWithAnimation = useCallback(() => {
setIsClosing(true);
setTimeout(() => {
setIsClosing(false);
onRequestClose();
}, 500);
}, [onRequestClose]);

const handleEscape = useCallback(
e => {
if (e.key === ‘Escape’) {
closeModalWithAnimation();
}
},
[closeModalWithAnimation]
);

useEffect(() => {
document.addEventListener(‘keydown’, handleEscape);

return () => {
  document.removeEventListener('keydown', handleEscape);
};

}, [handleEscape]);

const validateForm = () => {
const errors = {};
if (!newReview.name.trim()) {
errors.name = ‘Не введено имя.’;
}
if (!newReview.comment.trim()) {
errors.comment = ‘Не введен комментарий.’;
}
return errors;
};

const handleInputChange = e => {
const { name, value } = e.target;
setNewReview({ …newReview, [name]: value });
};

const handleSubmit = async e => {
e.preventDefault();

const errors = validateForm();
setFormErrors(errors);
if (Object.keys(errors).length === 0) {
  try {
    await addReviewApi(newReview);
    const reviews = await fetchReviewsApi();
    dispatch(setReviews(reviews));
    onSuccess();
    closeModalWithAnimation();
  } catch (error) {
    console.error('Ошибка при добавлении отзыва:', error);
  }
} else {
  Notiflix.Notify.failure('Будь ласка, заповніть усі поля форми.', {
    position: 'center-center',
    timeout: 2000,
  });
}

};

const overlayClassName = isOpen ? ‘modal_overlay active’ : ‘modal_overlay’;
const reviewClassName = modale_review ${isOpen ? 'active' : ''} ${ isClosing ? 'closing' : '' };

return (
<div className={${overlayClassName}} onClick={closeModalWithAnimation}>
<div className={${reviewClassName}} onClick={e => e.stopPropagation()}>

    <button className="close_modal" onClick={closeModalWithAnimation}>
      <svg className="icon_modal">
        <use xlinkHref={`${icons}#close`} />
      </svg>
    </button>
    <form className="reviews_form" onSubmit={handleSubmit}>
      <label className="reviews_label">
        Ім'я:
        <input
          className="reviews_input"
          type="text"
          name="name"
          value={newReview.name}
          autoComplete="name"
          onChange={handleInputChange}
        />
        {formErrors.name && (
          <div style={{ display: 'none' }}>{formErrors.name}</div>
        )}
      </label>
      <br />
      <label>
        Коментар:
        <textarea
          className="reviews_textarea"
          name="comment"
          value={newReview.comment}
          autoComplete="comment"
          onChange={handleInputChange}
        />
        {formErrors.comment && (
          <div style={{ display: 'none' }}>{formErrors.comment}</div>
        )}
      </label>
      <br />
      <button className="reviews_button modale" type="submit">
        ВІДПРАВИТИ ВІДГУК
      </button>
    </form>
  </div>
</div>

);
};

export default ModalReview;

/* !---------------Modal Review-------------------------- */

.modale_review {
position: fixed;
top: 50%;
left: 50%;
background-color: var(–background-slogan);
padding: 40px;
padding-top: 20px;
border-radius: 8px;
z-index: 1051;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
transition: transform var(–transition-timing) var(–transition-duration-product),
opacity var(–transition-timing) var(–transition-duration-product);
}

.modale_review.active {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}

.modale_review.closing {
transform: translate(-50%, -50%) scale(0);
opacity: 0;
transition: transform var(–transition-timing) var(–transition-duration-product),
opacity var(–transition-timing) var(–transition-duration-product);
}

@media screen and (max-width: 768px) {
.modale_review {
padding: 20px;
}
} и теперь анимация работает только при закрытии. я уже совсем ничего не понимаю…

Так как у меня нет желание настраивать проект чтобы пытаться запустить этот код, я не могу проверить конкретное поведение, и тем более отдебажить его. Если сможешь поделитсья рабочим кодом через какую-нить платформу типа jsfiddle, я подебажу-посмотрю. Вообще, этот подход - делиться запускаемым кодом (и с возможностью его меня-пробовать) - самый удобный для тех кто пытается помочь разобраться.

Мой совет остается тем же самым: пытаться остановить отрисовку страницы через debugger, и смотреть какие конкретно стили применились.