|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import {ReactNode, useEffect, useState} from 'react'; |
| 4 | +import {ChevronDownIcon, ChevronRightIcon} from '@radix-ui/react-icons'; |
| 5 | + |
| 6 | +// explicitly not usig CSS modules here |
| 7 | +// because there's some prerendered content that depends on these exact class names |
| 8 | +import '../callout/styles.scss'; |
| 9 | +import styles from './style.module.scss'; |
| 10 | + |
| 11 | +type Props = { |
| 12 | + children: ReactNode; |
| 13 | + title: string; |
| 14 | + /** If defined, the expandable will be grouped with other expandables that have the same group. */ |
| 15 | + group?: string; |
| 16 | + level?: 'info' | 'warning' | 'success'; |
| 17 | + permalink?: boolean; |
| 18 | +}; |
| 19 | + |
| 20 | +function slugify(str: string) { |
| 21 | + return str |
| 22 | + .toLowerCase() |
| 23 | + .replace(/ /g, '-') |
| 24 | + .replace(/[^a-z0-9-]/g, ''); |
| 25 | +} |
| 26 | + |
| 27 | +export function Expandable({title, level = 'info', children, permalink, group}: Props) { |
| 28 | + const id = permalink ? slugify(title) : undefined; |
| 29 | + |
| 30 | + const [isExpanded, setIsExpanded] = useState(false); |
| 31 | + |
| 32 | + // Ensure we scroll to the element if the URL hash matches |
| 33 | + useEffect(() => { |
| 34 | + if (!id) { |
| 35 | + return () => {}; |
| 36 | + } |
| 37 | + |
| 38 | + if (window.location.hash === `#${id}`) { |
| 39 | + document.querySelector(`#${id}`)?.scrollIntoView(); |
| 40 | + setIsExpanded(true); |
| 41 | + } |
| 42 | + |
| 43 | + // When the hash changes (e.g. when the back/forward browser buttons are used), |
| 44 | + // we want to ensure to jump to the correct section |
| 45 | + const onHashChange = () => { |
| 46 | + if (window.location.hash === `#${id}`) { |
| 47 | + setIsExpanded(true); |
| 48 | + document.querySelector(`#${id}`)?.scrollIntoView(); |
| 49 | + } |
| 50 | + }; |
| 51 | + // listen for hash changes and expand the section if the hash matches the title |
| 52 | + window.addEventListener('hashchange', onHashChange); |
| 53 | + return () => { |
| 54 | + window.removeEventListener('hashchange', onHashChange); |
| 55 | + }; |
| 56 | + }, [id]); |
| 57 | + |
| 58 | + function toggleIsExpanded(event: React.MouseEvent<HTMLDetailsElement>) { |
| 59 | + const newVal = event.currentTarget.open; |
| 60 | + |
| 61 | + if (id) { |
| 62 | + if (newVal) { |
| 63 | + window.history.pushState({}, '', `#${id}`); |
| 64 | + } else { |
| 65 | + window.history.pushState({}, '', '#'); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + setIsExpanded(newVal); |
| 70 | + } |
| 71 | + |
| 72 | + return ( |
| 73 | + <details |
| 74 | + name={group} |
| 75 | + className={`${styles.expandable} callout !block ${'callout-' + level}`} |
| 76 | + open={isExpanded} |
| 77 | + // We only need this to keep the URL hash in sync |
| 78 | + onToggle={id ? toggleIsExpanded : undefined} |
| 79 | + id={id} |
| 80 | + > |
| 81 | + <summary className={`${styles['expandable-header']} callout-header`}> |
| 82 | + <ChevronDownIcon |
| 83 | + className={`${styles['expandable-icon-expanded']} callout-icon`} |
| 84 | + /> |
| 85 | + <ChevronRightIcon |
| 86 | + className={`${styles['expandable-icon-collapsed']} callout-icon`} |
| 87 | + /> |
| 88 | + <div>{title}</div> |
| 89 | + </summary> |
| 90 | + <div className={`${styles['expandable-body']} callout-body content-flush-bottom`}> |
| 91 | + {children} |
| 92 | + </div> |
| 93 | + </details> |
| 94 | + ); |
| 95 | +} |
| 96 | + |
| 97 | +// |
| 98 | +// |
| 99 | +// <details name={group} className={`callout !block ${'callout-' + level}`}> |
| 100 | +// <summary className="callout-header">{title}</summary> |
| 101 | +// <div className="callout-body content-flush-bottom">{children}</div> |
| 102 | +// </details> |
| 103 | +// |
| 104 | +// |
| 105 | + |
| 106 | +// |
| 107 | +// <Callout |
| 108 | +// level={level} |
| 109 | +// title={title} |
| 110 | +// Icon={isExpanded ? ChevronDownIcon : ChevronRightIcon} |
| 111 | +// id={id} |
| 112 | +// titleOnClick={toggleIsExpanded} |
| 113 | +// > |
| 114 | +// {isExpanded ? children : undefined} |
| 115 | +// </Callout> |
0 commit comments