woocommerce/plugins/woocommerce-blocks/assets/js/base/components/read-more/index.tsx

248 lines
5.1 KiB
TypeScript

/**
* External dependencies
*/
import { createRef, Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import type { MouseEvent, RefObject, ReactNode } from 'react';
/**
* Internal dependencies
*/
import { clampLines } from './utils';
export interface ReadMoreProps {
/**
* The entire content to clamp
*/
children: ReactNode;
/**
* Class names for the wrapped component
*/
className: string;
/**
* What symbol to show after the allowed lines are reached
*
* @default '&hellip';
*/
ellipsis: string;
/**
* The string to show to collapse the entire text into its clamped form
*
* @default 'Read less'
*/
lessText: string;
/**
* How many lines to show before the text is clamped
*
* @default 3
*/
maxLines: number;
/**
* The string to show to expande the entire text
*
* @default 'Read more'
*/
moreText: string;
}
interface ReadMoreState {
/**
* This is true when read more has been pressed and the full review is shown.
*/
isExpanded: boolean;
/**
* True if we are clamping content. False if the review is short. Null during init.
*/
clampEnabled: boolean | null;
/**
* Content is passed in via children.
*/
content: ReactNode;
/**
* Summary content generated from content HTML.
*/
summary: string;
}
export const defaultProps = {
className: 'read-more-content',
ellipsis: '…',
lessText: __( 'Read less', 'woocommerce' ),
maxLines: 3,
moreText: __( 'Read more', 'woocommerce' ),
};
/**
* Show text based content, limited to a number of lines, with a read more link.
*
* Based on https://github.com/zoltantothcom/react-clamp-lines.
*/
class ReadMore extends Component< ReadMoreProps, ReadMoreState > {
static defaultProps = defaultProps;
private reviewSummary: RefObject< HTMLDivElement >;
private reviewContent: RefObject< HTMLDivElement >;
constructor( props: ReadMoreProps ) {
super( props );
this.state = {
/**
* This is true when read more has been pressed and the full review is shown.
*/
isExpanded: false,
/**
* True if we are clamping content. False if the review is short. Null during init.
*/
clampEnabled: null,
/**
* Content is passed in via children.
*/
content: props.children,
/**
* Summary content generated from content HTML.
*/
summary: '.',
};
this.reviewContent = createRef< HTMLDivElement >();
this.reviewSummary = createRef< HTMLDivElement >();
this.getButton = this.getButton.bind( this );
this.onClick = this.onClick.bind( this );
}
componentDidMount(): void {
this.setSummary();
}
componentDidUpdate( prevProps: ReadMoreProps ): void {
if (
prevProps.maxLines !== this.props.maxLines ||
prevProps.children !== this.props.children
) {
/**
* if maxLines or content changed we need to reset the state to
* initial values so that summary can be calculated again
*/
this.setState(
{
clampEnabled: null,
summary: '.',
},
this.setSummary
);
}
}
setSummary(): void {
if ( this.props.children ) {
const { maxLines, ellipsis } = this.props;
if (
! this.reviewSummary.current ||
! this.reviewContent.current
) {
return;
}
const lineHeight = this.reviewSummary.current.clientHeight + 1;
const reviewHeight = this.reviewContent.current.clientHeight + 1;
const maxHeight = lineHeight * maxLines + 1;
const clampEnabled = reviewHeight > maxHeight;
this.setState( {
clampEnabled,
} );
if ( clampEnabled ) {
this.setState( {
summary: clampLines(
this.reviewContent.current.innerHTML,
this.reviewSummary.current,
maxHeight,
ellipsis
),
} );
}
}
}
getButton(): JSX.Element | undefined {
const { isExpanded } = this.state;
const { className, lessText, moreText } = this.props;
const buttonText = isExpanded ? lessText : moreText;
if ( ! buttonText ) {
return;
}
return (
<a
href="#more"
className={ className + '__read_more' }
onClick={ this.onClick }
aria-expanded={ ! isExpanded }
role="button"
>
{ buttonText }
</a>
);
}
/**
* Handles the click event for the read more/less button.
*/
onClick( e: MouseEvent< HTMLAnchorElement, MouseEvent > ): void {
e.preventDefault();
const { isExpanded } = this.state;
this.setState( {
isExpanded: ! isExpanded,
} );
}
render(): JSX.Element | null {
const { className } = this.props;
const { content, summary, clampEnabled, isExpanded } = this.state;
if ( ! content ) {
return null;
}
if ( clampEnabled === false ) {
return (
<div className={ className }>
<div ref={ this.reviewContent }>{ content }</div>
</div>
);
}
return (
<div className={ className }>
{ ( ! isExpanded || clampEnabled === null ) && (
<div
ref={ this.reviewSummary }
aria-hidden={ isExpanded }
dangerouslySetInnerHTML={ {
__html: summary,
} }
/>
) }
{ ( isExpanded || clampEnabled === null ) && (
<div
ref={ this.reviewContent }
aria-hidden={ ! isExpanded }
>
{ content }
</div>
) }
{ this.getButton() }
</div>
);
}
}
export default ReadMore;