@@ -5,21 +5,48 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'preact/hooks';
55
66import { observeElementSize } from '../utils/observe-element-size' ;
77
8- type InlineControlsProps = {
8+ type InlineControlExcerptProps = {
9+ /**
10+ * The excerpt provides internal controls to expand and collapse
11+ * the content.
12+ */
13+ inlineControl : true ;
14+
15+ /**
16+ * Text on inline control when clicking it will expand the content.
17+ * Defaults to 'More'.
18+ */
19+ inlineControlExpandText ?: string ;
20+
21+ /**
22+ * Text on inline control when clicking it will collapse the content.
23+ * Defaults to 'Less'.
24+ */
25+ inlineControlCollapseText ?: string ;
26+
27+ /** Additional styles to pass to the inline controls element. */
28+ inlineControlStyle ?: JSX . CSSProperties ;
29+ /** Additional CSS classes to pass to the inline controls element. */
30+ inlineControlClasses ?: string | string [ ] ;
31+ } ;
32+
33+ type InlineControlProps = InlineControlExcerptProps & {
934 isCollapsed : boolean ;
1035 setCollapsed : ( collapsed : boolean ) => void ;
11- linkStyle : JSX . CSSProperties ;
1236} ;
1337
1438/**
1539 * An optional toggle link at the bottom of an excerpt which controls whether
1640 * it is expanded or collapsed.
1741 */
18- function InlineControls ( {
42+ function InlineControl ( {
1943 isCollapsed,
2044 setCollapsed,
21- linkStyle,
22- } : InlineControlsProps ) {
45+ inlineControlExpandText = 'More' ,
46+ inlineControlCollapseText = 'Less' ,
47+ inlineControlStyle,
48+ inlineControlClasses,
49+ } : InlineControlProps ) {
2350 return (
2451 < div
2552 className = { classnames (
@@ -42,40 +69,30 @@ function InlineControls({
4269 onClick = { ( ) => setCollapsed ( ! isCollapsed ) }
4370 expanded = { ! isCollapsed }
4471 title = "Toggle visibility of full excerpt text"
45- style = { linkStyle }
72+ style = { inlineControlStyle }
73+ classes = { inlineControlClasses }
4674 underline = "always"
4775 inline
4876 >
49- { isCollapsed ? 'More' : 'Less' }
77+ { isCollapsed ? inlineControlExpandText : inlineControlCollapseText }
5078 </ LinkButton >
5179 </ div >
5280 </ div >
5381 ) ;
5482}
5583
56- const noop = ( ) => { } ;
57-
58- export type ExcerptProps = {
59- children ?: ComponentChildren ;
60-
84+ type ExternalControlExcerptProps = {
6185 /**
62- * If `true`, the excerpt provides internal controls to expand and collapse
63- * the content. If `false`, the caller sets the collapsed state via the
64- * `collapse` prop. When using inline controls, the excerpt is initially
65- * collapsed.
86+ * The caller is responsible for providing their own collapse/expand control,
87+ * in combination with `collapsed` and `onToggleCollapsed` props.
6688 */
67- inlineControls ?: boolean ;
89+ inlineControl : false ;
90+ } ;
6891
69- /**
70- * If the content should be truncated if its height exceeds
71- * `collapsedHeight + overflowThreshold`. This prop is only used if
72- * `inlineControls` is false.
73- */
74- collapse ?: boolean ;
92+ export type ExcerptProps = {
93+ children ?: ComponentChildren ;
7594
76- /**
77- * Maximum height of the container, in pixels, when it is collapsed.
78- */
95+ /** Maximum height of the container, in pixels, when it is collapsed. */
7996 collapsedHeight : number ;
8097
8198 /**
@@ -85,45 +102,78 @@ export type ExcerptProps = {
85102 overflowThreshold ?: number ;
86103
87104 /**
88- * Called when the content height exceeds or falls below
105+ * Whether a shadow is drawn at the bottom of collapsed content, to hint that
106+ * content is being hidden.
107+ *
108+ * The shadow area can be clicked to expand the container so the content is
109+ * fully visible.
110+ *
111+ * Defaults to `false` for excerpts with inline control, and `true` for
112+ * excerpts with external control.
113+ */
114+ shadow ?: boolean ;
115+
116+ /**
117+ * If the content should be truncated if its height exceeds
89118 * `collapsedHeight + overflowThreshold`.
119+ *
120+ * Use this prop in combination with `onToggleCollapsed` to make this excerpt
121+ * a controlled component.
122+ *
123+ * Defaults to `true`.
90124 */
91- onCollapsibleChanged ?: ( isCollapsible : boolean ) => void ;
125+ collapsed ?: boolean ;
92126
93127 /**
94- * When `inlineControls` is `false`, this function is called when the user
95- * requests to expand the content by clicking a zone at the bottom of the
96- * container.
128+ * If this function is provided, it is called when the user requests to expand
129+ * the content by clicking the shadowed zone at the bottom of the container
130+ * (if `shadow` is `true`) or the inline control (if `inlineControl` is `true`)
97131 */
98132 onToggleCollapsed ?: ( collapsed : boolean ) => void ;
99133
100134 /**
101- * Additional styles to pass to the inline controls element.
102- * Ignored if inlineControls is `false `.
135+ * Called when the content height exceeds or falls below
136+ * `collapsedHeight + overflowThreshold `.
103137 */
104- inlineControlsLinkStyle ?: JSX . CSSProperties ;
105- } ;
138+ onCollapsibleChanged ?: ( isCollapsible : boolean ) => void ;
139+ } & ( InlineControlExcerptProps | ExternalControlExcerptProps ) ;
106140
107141/**
108142 * A container which truncates its content when they exceed a specified height.
109143 *
110144 * The collapsed state of the container can be handled either via internal
111- * controls (if `inlineControls` is `true`) or by the caller using the
112- * `collapse` prop .
145+ * controls (if `inlineControls` is `true`) or by the caller using a custom
146+ * control .
113147 */
114148export default function Excerpt ( {
115149 children,
116- collapse = false ,
150+ collapsed ,
117151 collapsedHeight,
118- inlineControls = true ,
119- onCollapsibleChanged = noop ,
120- onToggleCollapsed = noop ,
152+ onCollapsibleChanged,
153+ onToggleCollapsed,
121154 overflowThreshold = 0 ,
122- inlineControlsLinkStyle = { } ,
155+ shadow,
156+ ...rest
123157} : ExcerptProps ) {
124- const [ collapsedByInlineControls , setCollapsedByInlineControls ] =
125- useState ( true ) ;
158+ // If `collapsed` is present, treat this as a controlled component
159+ const isControlled = typeof collapsed === 'boolean' ;
160+ // Only use this local state if Excerpt is uncontrolled
161+ const [ uncontrolledCollapsed , setUncontrolledCollapsed ] = useState (
162+ collapsed ?? true ,
163+ ) ;
164+ const isCollapsed = isControlled ? collapsed : uncontrolledCollapsed ;
165+ const setCollapsed = useCallback (
166+ ( collapsed : boolean ) => {
167+ onToggleCollapsed ?.( collapsed ) ;
168+ if ( ! isControlled ) {
169+ setUncontrolledCollapsed ( collapsed ) ;
170+ }
171+ } ,
172+ [ isControlled , onToggleCollapsed ] ,
173+ ) ;
126174
175+ const inlineControl = rest . inlineControl ;
176+ const withShadow = shadow ?? ! inlineControl ;
127177 const contentElement = useRef < HTMLDivElement | null > ( null ) ;
128178
129179 // Measured height of `contentElement` in pixels
@@ -138,7 +188,7 @@ export default function Excerpt({
138188 // prettier-ignore
139189 const isCollapsible =
140190 newContentHeight > ( collapsedHeight + overflowThreshold ) ;
141- onCollapsibleChanged ( isCollapsible ) ;
191+ onCollapsibleChanged ?. ( isCollapsible ) ;
142192 } , [ collapsedHeight , onCollapsibleChanged , overflowThreshold ] ) ;
143193
144194 useLayoutEffect ( ( ) => {
@@ -154,19 +204,13 @@ export default function Excerpt({
154204 // expanding/collapsing the content.
155205 // prettier-ignore
156206 const isOverflowing = contentHeight > ( collapsedHeight + overflowThreshold ) ;
157- const isCollapsed = inlineControls ? collapsedByInlineControls : collapse ;
158207 const isExpandable = isOverflowing && isCollapsed ;
159208
160- const contentStyle : Record < string , number > = { } ;
209+ const contentStyle : JSX . CSSProperties = { } ;
161210 if ( contentHeight !== 0 ) {
162- contentStyle [ 'max-height' ] = isExpandable ? collapsedHeight : contentHeight ;
211+ contentStyle . maxHeight = isExpandable ? collapsedHeight : contentHeight ;
163212 }
164213
165- const setCollapsed = ( collapsed : boolean ) =>
166- inlineControls
167- ? setCollapsedByInlineControls ( collapsed )
168- : onToggleCollapsed ( collapsed ) ;
169-
170214 return (
171215 < div
172216 data-testid = "excerpt-container"
@@ -200,23 +244,23 @@ export default function Excerpt({
200244 'transition-[opacity] duration-150 ease-linear' ,
201245 'absolute w-full bottom-0 h-touch-minimum' ,
202246 {
203- // For expandable excerpts not using inlineControls , style this
204- // element with a shadow-like gradient
247+ // For expandable excerpts with shadow , style this element with a
248+ // shadow-like gradient
205249 'bg-gradient-to-b from-white/0 via-95% via-black/10 to-100% to-black/15' :
206- ! inlineControls && isExpandable ,
207- 'bg-none' : inlineControls ,
208- // Don't make this shadow visible OR clickable if there's nothing
250+ withShadow && isExpandable ,
251+ 'bg-none' : ! withShadow ,
252+ // Don't make the shadow visible OR clickable if there's nothing
209253 // to do here (the excerpt isn't expandable)
210254 'opacity-0 pointer-events-none' : ! isExpandable ,
211255 } ,
212256 ) }
213257 title = "Show the full excerpt"
214258 />
215- { isOverflowing && inlineControls && (
216- < InlineControls
217- isCollapsed = { collapsedByInlineControls }
259+ { isOverflowing && inlineControl && (
260+ < InlineControl
261+ isCollapsed = { isCollapsed }
218262 setCollapsed = { setCollapsed }
219- linkStyle = { inlineControlsLinkStyle }
263+ { ... rest }
220264 />
221265 ) }
222266 </ div >
0 commit comments