1 2import { useEffect, useMemo, useRef, useState } from 'react'; 3import { html } from '../html.js?v=3.8.36'; 4import { buildTimelineModel } from '../utils/timelineData.js?v=3.8.36'; 5import { ChevronDown, ChevronUp } from 'lucide-react'; 6 7const Timeline = ({ records, selectedYear, onSelectYear }) => { 8 const [isExpanded, setIsExpanded] = useState(() => ( 9 typeof window === 'undefined' || window.innerWidth >= 640 10 )); 11 const [hoveredYear, setHoveredYear] = useState(null); 12 const [rovingYear, setRovingYear] = useState(null); 13 const yearButtonRefs = useRef(new Map()); 14 15 const { timelineData, dataMinYear, dataMaxYear } = useMemo( 16 () => buildTimelineModel(records), 17 [records], 18 ); 19 20 const enabledYears = useMemo( 21 () => timelineData.filter(data => data.count > 0).map(data => data.year), 22 [timelineData], 23 ); 24 25 useEffect(() => { 26 setRovingYear(current => { 27 if (selectedYear && enabledYears.includes(selectedYear)) return selectedYear; 28 if (current && enabledYears.includes(current)) return current; 29 return enabledYears[0] || null; 30 }); 31 }, [enabledYears, selectedYear]); 32 33 if (timelineData.length === 0) return null; 34 35 const maxCount = Math.max(...timelineData.map(d => d.count)); 36 const logMax = maxCount > 0 ? Math.log(maxCount + 1) : 1; 37 38 const handleYearKeyDown = (event, year) => { 39 if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; 40 event.preventDefault(); 41 const currentIndex = enabledYears.indexOf(year); 42 let nextYear; 43 if (event.key === 'Home') nextYear = enabledYears[0]; 44 else if (event.key === 'End') nextYear = enabledYears[enabledYears.length - 1]; 45 else { 46 const direction = event.key === 'ArrowRight' ? 1 : -1; 47 nextYear = enabledYears[(currentIndex + direction + enabledYears.length) % enabledYears.length]; 48 } 49 setRovingYear(nextYear); 50 yearButtonRefs.current.get(nextYear)?.focus(); 51 }; 52 53 return html` 54 <section className="archive-timeline" aria-labelledby="archive-timeline-title"> 55 <div className="archive-timeline__header"> 56 <button 57 type="button" 58 onClick=${() => setIsExpanded(!isExpanded)} 59 className="archive-timeline__toggle" 60 aria-expanded=${isExpanded} 61 aria-controls="archive-timeline-panel" 62 > 63 <h3 id="archive-timeline-title">Timeline</h3> 64 <span className="archive-timeline__range"> 65 ${dataMinYear}â${dataMaxYear} 66 </span> 67 ${isExpanded 68 ? html`<${ChevronUp} className="w-4 h-4" aria-hidden="true" />` 69 : html`<${ChevronDown} className="w-4 h-4" aria-hidden="true" />`} 70 </button> 71 ${selectedYear && html` 72 <button 73 type="button" 74 onClick=${() => onSelectYear(null)} 75 className="archive-action archive-action--quiet archive-timeline__clear" 76 > 77 Clear ${selectedYear} 78 </button> 79 `} 80 </div> 81 82 ${isExpanded && html` 83 <div id="archive-timeline-panel" className="archive-panel archive-density--compact archive-timeline__panel"> 84 <div 85 className="archive-timeline__scroll" 86 role="region" 87 aria-label="Archive timeline by year" 88 > 89 ${timelineData.map((data) => { 90 const heightPercent = maxCount > 0 91 ? (data.count > 0 ? Math.max((Math.log(data.count + 1) / logMax) * 100, 8) : 0) 92 : 0; 93 const isSelected = selectedYear === data.year; 94 const isHovered = hoveredYear === data.year; 95 const isYearLabel = parseInt(data.year) % 5 === 0 96 || data.year === timelineData[0].year 97 || data.year === timelineData[timelineData.length - 1].year; 98 99 return html` 100 <button 101 type="button" 102 key=${data.year} 103 ref=${element => { 104 if (element) yearButtonRefs.current.set(data.year, element); 105 else yearButtonRefs.current.delete(data.year); 106 }} 107 disabled=${data.count === 0} 108 tabIndex=${data.year === rovingYear ? 0 : -1} 109 onClick=${() => onSelectYear(isSelected ? null : data.year)} 110 onKeyDown=${event => handleYearKeyDown(event, data.year)} 111 onMouseEnter=${() => setHoveredYear(data.year)} 112 onMouseLeave=${() => setHoveredYear(null)} 113 onFocus=${() => { 114 setHoveredYear(data.year); 115 setRovingYear(data.year); 116 }} 117 onBlur=${() => setHoveredYear(null)} 118 className="archive-timeline__year" 119 data-selected=${isSelected ? 'true' : 'false'} 120 aria-label=${`${data.year}: ${data.count} record${data.count === 1 ? '' : 's'}`} 121 aria-pressed=${isSelected} 122 >
123 <span className="archive-timeline__tooltip" aria-hidden="true"> 124 <strong>${data.year}</strong> 125 <span>${data.count} records</span> 126 </span> 127 <span 128 className=${`archive-timeline__bar ${isHovered ? 'is-hovered' : ''}`} 129 style=${{ height: data.count > 0 ? `${heightPercent}%` : '0px' }} 130 aria-hidden="true" 131 /> 132 ${isYearLabel && html`<span className="archive-timeline__tick" aria-hidden="true" />`} 133 ${isYearLabel && html` 134 <span className="archive-timeline__year-label" aria-hidden="true">${data.year}</span> 135 `} 136 </button> 137 `; 138 })} 139 </div> 140 </div> 141 `} 142 </section> 143 `; 144}; 145 146export default Timeline;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.