Add expandable content for Object component

Refactored the Object component to allow expandable/collapsible content when the description or address overflows the container. Added dynamic height detection and a gradient fade for collapsed state, with responsive max-height adjustments in SCSS. Improved user experience for long content display.
This commit is contained in:
Madara0330E
2025-07-25 17:39:55 +05:00
parent f6c3dc3e6b
commit e36645ab6f
2 changed files with 142 additions and 25 deletions

View File

@@ -1,26 +1,59 @@
import "./Object.scss"; import "./Object.scss";
import { useState } from "react"; import { useState, useEffect, useRef } from "react";
function Object(props) { function Object(props) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [needsExpansion, setNeedsExpansion] = useState(false);
const contentRef = useRef(null);
const addressLimit = 30; const checkIfContentOverflows = () => {
const isAddressTooLong = props.address && props.address.length > addressLimit; if (contentRef.current) {
const needsExpansion = isAddressTooLong; // Временно убираем класс collapsed для измерения полной высоты
const element = contentRef.current;
const hadCollapsedClass = element.classList.contains(
"item__content--collapsed"
);
const truncateAddress = (text) => { if (hadCollapsedClass) {
if (!text) return ""; element.classList.remove("item__content--collapsed");
if (isExpanded) return text; }
if (text.length <= addressLimit) return text;
const truncated = text.substring(0, addressLimit); // Измеряем полную высоту
const lastSpaceIndex = truncated.lastIndexOf(" "); const fullHeight = element.scrollHeight;
if (lastSpaceIndex > 0) {
return truncated.substring(0, lastSpaceIndex) + "..."; // Возвращаем класс обратно
if (hadCollapsedClass) {
element.classList.add("item__content--collapsed");
}
// Измеряем высоту в свернутом состоянии
const collapsedHeight = element.clientHeight;
// Проверяем, есть ли разница больше чем 20px (чтобы избежать ложных срабатываний)
const heightDifference = fullHeight - collapsedHeight;
setNeedsExpansion(heightDifference > 20);
} }
return truncated + "...";
}; };
useEffect(() => {
// Задержка для правильного расчета после рендера
const timeoutId = setTimeout(() => {
checkIfContentOverflows();
}, 200);
// Проверяем при изменении размера окна
const handleResize = () => {
setTimeout(checkIfContentOverflows, 100);
};
window.addEventListener("resize", handleResize);
return () => {
clearTimeout(timeoutId);
window.removeEventListener("resize", handleResize);
};
}, [props.desc, props.address]);
const toggleExpansion = () => { const toggleExpansion = () => {
setIsExpanded(!isExpanded); setIsExpanded(!isExpanded);
}; };
@@ -29,23 +62,31 @@ function Object(props) {
<div className={`item ${isExpanded ? "item--expanded" : ""}`}> <div className={`item ${isExpanded ? "item--expanded" : ""}`}>
<img className="item__image" src={props.image} alt="квартира" /> <img className="item__image" src={props.image} alt="квартира" />
<div className="item__info font-inter-bold"> <div className="item__info font-inter-bold">
<div className="item__content"> <div
className={`item__content ${
!isExpanded ? "item__content--collapsed" : ""
}`}
ref={contentRef}
>
<p className="item__price">{props.price}</p> <p className="item__price">{props.price}</p>
<p className="item__desc" title={props.desc}> <p className="item__desc" title={props.desc}>
{props.desc} {props.desc}
</p> </p>
<p className="item__address font-inter-regular" title={props.address}> <p className="item__address font-inter-regular" title={props.address}>
{truncateAddress(props.address)} {props.address}
</p> </p>
</div> </div>
{needsExpansion && ( {needsExpansion && (
<button <div className="item__expand-wrapper">
className="item__expand-btn" <button
onClick={toggleExpansion} className="item__expand-btn"
type="button" onClick={toggleExpansion}
> type="button"
{isExpanded ? "Скрыть" : "Показать больше"} >
</button> {isExpanded ? "Скрыть" : "Показать больше"}
</button>
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -191,6 +191,78 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 10px;
transition: max-height 0.3s ease, opacity 0.2s ease;
&--collapsed {
max-height: 180px;
overflow: hidden;
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20px;
background: linear-gradient(transparent, white);
pointer-events: none;
}
@media (min-width: 1800px) {
max-height: 200px;
}
@media (max-width: 1300px) {
max-height: 170px;
}
@media (max-width: 1200px) {
max-height: 160px;
}
@media (max-width: 1100px) {
max-height: 150px;
}
@media (max-width: $laptopWidth) {
max-height: 140px;
}
@media (max-width: $tabletWidth) {
max-height: 190px;
}
@media (max-width: 740px) {
max-height: 180px;
}
@media (max-width: 690px) {
max-height: 170px;
}
@media (max-width: 635px) {
max-height: 160px;
}
@media (max-width: 570px) {
max-height: 150px;
}
@media (max-width: $mobileWidth) {
max-height: 160px;
}
@media (max-width: 420px) {
max-height: 150px;
}
}
}
.item__expand-wrapper {
margin-top: auto;
padding-top: 5px;
} }
.item__price { .item__price {
@@ -239,12 +311,11 @@
.item__expand-btn { .item__expand-btn {
background: none; background: none;
border: none; border: none;
color: #007bff; color: #007bff !important;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
padding: 4px 0; padding: 4px 0;
margin-top: auto;
margin-bottom: 5px; margin-bottom: 5px;
text-align: left; text-align: left;
transition: color 0.2s ease; transition: color 0.2s ease;
@@ -256,12 +327,17 @@
text-overflow: ellipsis; text-overflow: ellipsis;
&:hover { &:hover {
color: #0056b3; color: #0056b3 !important;
text-decoration: underline; text-decoration: underline;
} }
&:focus { &:focus {
outline: none; outline: none;
color: #007bff !important;
}
&:visited {
color: #007bff !important;
} }
@media (min-width: 1800px) { @media (min-width: 1800px) {