Skip to content
This repository was archived by the owner on Dec 4, 2025. It is now read-only.
Merged
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
118 changes: 95 additions & 23 deletions src/components/Schedule/Event.jsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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}
}
`
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 (
Expand All @@ -155,11 +223,15 @@ const Event = ({ event }) => {
{formatTime(event.startTime)} - {formatTime(event.endTime)}
</TimeStamp>
<EventLocation>{event.location}</EventLocation>
<Points>{event.points && `Points: ${event.points}`}</Points>
{event.points > 0 && <Points>Points: ${event.points}</Points>}
<EventDescription ref={descriptionRef} expanded={expanded} maxHeight={maxHeight}>
{event.description}
</EventDescription>
{showToggleButton && <ToggleButton onClick={toggleExpanded} expanded={expanded} />}
{showToggleButton && (
<ToggleButton onClick={toggleExpanded} expanded={expanded} aria-label="Toggle description">
<img src={expandButton} alt="expand" />
</ToggleButton>
)}
</EventCard>
)
}
Expand Down