diff --git a/src/components/Schedule/Event.jsx b/src/components/Schedule/Event.jsx
index e88d38c2..0fba9de1 100644
--- a/src/components/Schedule/Event.jsx
+++ b/src/components/Schedule/Event.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef } from 'react'
+import React, { useState, useEffect, useLayoutEffect, useRef } from 'react'
import styled from 'styled-components'
import { P, H3 } from '../Typography'
import { Card, ScrollbarLike } from '../Common'
@@ -15,10 +15,10 @@ const EventDescription = styled(P)`
display: -webkit-box;
-webkit-line-clamp: ${props => (props.expanded ? 'unset' : '3')};
-webkit-box-orient: vertical;
- max-height: ${props => (props.expanded ? 'none' : '4.5em')};
+ max-height: ${props => (props.expanded ? `${props.maxHeight}px` : '4.5em')};
transition: max-height 0.3s ease;
${p => p.theme.mediaQueries.mobile} {
- overflow-y: scroll;
+ overflow-y: auto;
${ScrollbarLike}
}
`
@@ -35,19 +35,25 @@ const Points = styled(P)`
`
const ToggleButton = styled.button`
- background-image: url(${expandButton});
- background-color: transparent;
+ background: transparent;
border: none;
position: absolute;
cursor: pointer;
- width: 15px;
- height: 15px;
- background-size: contain;
- background-repeat: no-repeat;
- transform: ${props => (props.expanded ? 'rotate(180deg)' : 'rotate(0deg)')};
- transition: transform 0.3s ease;
+ width: 28px;
+ height: 28px;
right: 15px;
bottom: 15px;
+ z-index: 3;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ & > img {
+ width: 15px;
+ height: 15px;
+ transform: ${props => (props.expanded ? 'rotate(180deg)' : 'rotate(0deg)')};
+ transition: transform 0.3s ease;
+ display: block;
+ }
${p => p.theme.mediaQueries.mobile} {
display: none;
}
@@ -121,19 +127,81 @@ const Event = ({ event }) => {
const descriptionRef = useRef(null)
const theme = useTheme()
- useEffect(() => {
- if (descriptionRef.current) {
- const isOverflowing =
- descriptionRef.current.scrollHeight > descriptionRef.current.clientHeight
- setShowToggleButton(isOverflowing)
+ useLayoutEffect(() => {
+ const el = descriptionRef.current
+ if (!el) return
+
+ const clampLines = 3
+
+ const measureFullHeight = node => {
+ const clone = node.cloneNode(true)
+ // keep same wrapping so measurement matches on-card layout
+ clone.style.width = `${node.clientWidth}px`
+ clone.style.position = 'absolute'
+ clone.style.visibility = 'hidden'
+ clone.style.pointerEvents = 'none'
+ clone.style.maxHeight = 'none'
+ clone.style.webkitLineClamp = 'unset'
+ clone.style.display = 'block'
+ clone.style.boxSizing = 'border-box'
+ document.body.appendChild(clone)
+ const h = clone.scrollHeight
+ document.body.removeChild(clone)
+ return h
}
- }, [descriptionRef, event.description])
- const toggleExpanded = () => {
- setExpanded(!expanded)
- if (!expanded) {
- setMaxHeight(descriptionRef.current.scrollHeight)
+ const checkOverflow = () => {
+ const style = getComputedStyle(el)
+ const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2
+ const allowedHeight = lineHeight * clampLines
+ const fullHeight = measureFullHeight(el)
+ const isOverflowed = fullHeight >= Math.ceil(allowedHeight)
+ setShowToggleButton(isOverflowed)
+ }
+
+ // initial check on next paint
+ const rafId = requestAnimationFrame(checkOverflow)
+
+ // re-check when the element or its parent resizes (fonts, layout, CSS)
+ let ro
+ if (typeof ResizeObserver !== 'undefined') {
+ ro = new ResizeObserver(checkOverflow)
+ ro.observe(el)
+ if (el.parentElement) ro.observe(el.parentElement)
+ }
+
+ // re-check after fonts load (if supported)
+ if (document?.fonts && document.fonts.ready) {
+ document.fonts.ready.then(checkOverflow).catch(() => {})
}
+
+ // when expanded, measure and set the full pixel height (with a small buffer)
+ // to ensure the animated max-height is never slightly short of the content.
+ const HEIGHT_BUFFER = 6 // extra pixels to avoid 1-2px clipping on some browsers
+ const setMeasuredHeightWhenExpanded = () => {
+ if (!expanded) return
+ // measure via clone to avoid mutating the live node
+ const fullHeight = measureFullHeight(el)
+ // add a small buffer so rounding doesn't clip the last line
+ setMaxHeight(fullHeight + HEIGHT_BUFFER)
+ }
+
+ // set measured height initially if already expanded
+ const rafSetHeight = requestAnimationFrame(setMeasuredHeightWhenExpanded)
+
+ const onResize = () => checkOverflow()
+ window.addEventListener('resize', onResize)
+
+ return () => {
+ cancelAnimationFrame(rafId)
+ cancelAnimationFrame(rafSetHeight)
+ if (ro) ro.disconnect()
+ window.removeEventListener('resize', onResize)
+ }
+ }, [event.description, expanded])
+
+ const toggleExpanded = () => {
+ setExpanded(prev => !prev)
}
return (
@@ -155,11 +223,15 @@ const Event = ({ event }) => {
{formatTime(event.startTime)} - {formatTime(event.endTime)}
{event.location}
- {event.points && `Points: ${event.points}`}
+ {event.points > 0 && Points: ${event.points}}
{event.description}
- {showToggleButton && }
+ {showToggleButton && (
+
+
+
+ )}
)
}