N 行以上なら省略表示したい
所感: LGTM
import { FC, ReactNode, CSSProperties } from "react"; import styles from "./LineClampingBox.module.scss"; /** 指定した行数以上なら省略表示する */ export const LineClampingBox: FC<{ /** 最大行数 */ maxLines: number; children: ReactNode; }> = ({ maxLines, children }) => { return ( <div className={styles.box} // CSS variables で値を渡す style={{ "--maxLines": maxLines } as CSSProperties} > {children} </div> ); };
.box { // -webkit- prefix を使い続けるのはやや微妙だが, 現状はこれが最適と思われる display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--maxLines); overflow: hidden; }
N 行以上なら省略表示しつつ, 省略され得る時は開閉ボタンを表示したい
所感: 実装を読み書きするコストが必要以上に高いかもです. 本当に「N 行以上なら省略表示」が重要なのであればこれでもよいですが, もし可能なら他の手段も検討できませんか?
import { FC, ReactNode, CSSProperties, useState, useRef, useEffect } from "react"; import clsx from "clsx"; import styles from "./LineClampingBox.module.scss"; /** 指定した行数以上なら省略表示する */ export const LineClampingBox: FC<{ /** 省略時の最大行数 */ maxLines: number; /** * 注意: ステートフルなコンポーネントを children に渡さないこと. * LineClampingBox は省略表示するのに十分な行数があるかを判定するために, 二重に children を描画する. * もしステートフルなコンポーネントが存在すると状態がずれてしまい, 誤った判定をしてしまう可能性がある. * (二重に children を描画するのではなく, 描画された要素をコピーすれば解消するかも?) */ children: ReactNode; }> = ({ maxLines, children }) => { // hasManyLines = 省略表示をするのに十分な行数があるか = 開閉ボタンを表示すべきか. // 描画前に計算することは不可能なので, 描画した結果を元に判定する. // CSS だけで判定できると JS の実行を待たずに済むので layout shift が減らせるのだが, // そういった手段は現状ではなさそうに見える. const [hasManyLines, setHasManyLines] = useState(false); const hiddenTextRef = useRef<HTMLDivElement | null>(null); // 判定は要素がリサイズされたときか children が変化したときに行えば良い. // ResizeObserver を使いつつ, children が変化したときは再判定させるために useEffect の依存配列に children を入れている. useEffect(() => { const elem = hiddenTextRef.current; if (!elem) { return () => {}; } const observer = new ResizeObserver(() => { // 省略表示されているときは scrollHeight > clientHeight になっている // 参考: https://stackoverflow.com/questions/52169520/how-can-i-check-whether-line-clamp-is-enabled setHasManyLines(elem.scrollHeight > elem.clientHeight); }); observer.observe(elem); return () => { observer.disconnect(); }; }, [children]); const [isOpen, setIsOpen] = useState(false); return ( <div className={styles.box} // CSS variables で値を渡す style={{ "--maxLines": maxLines } as CSSProperties} > <div className={clsx(styles.children, isOpen && styles.open)}> {children} </div> {/* * hasManyLines の判定に使う非表示の children. * inert, aria-hidden 属性を付けてインタラクションを無効化しておく. */} <div className={clsx(styles.children, styles.hidden)} inert aria-hidden ref={hiddenTextRef}> {children} </div> {hasManyLines && ( <div> {isOpen ? ( <button type="button" onClick={() => { setIsOpen(false); }}> show less </button> ) : ( <button type="button" onClick={() => { setIsOpen(true); }}> show more </button> )} </div> )} </div> ); };
.box { position: relative; } .children { // -webkit- prefix を使い続けるのはやや微妙だが, 現状はこれが最適と思われる display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--maxLines); overflow: hidden; // display: none だとサイズが測れないので, opacity: 0 で透明化する. // そのままだと領域をとってしまうので, position: absolute で領域をとらないようにする. &.hidden { position: absolute; opacity: 0; } &.open { -webkit-line-clamp: none; } }
次回: N 文字以上なら省略表示したい
「文字」とは?