Txt always sets an absolute line height; RenderTxt never does. Row pins every cell and stacks when the fill column would be too narrow. IdText and FitText shrink then chunk or truncate. The fontkit measurer matches react-pdf text widths exactly. A TypeScript-AST lint test enforces the layout rules (no lineHeight outside Txt, no non-literal fixed/wrap/minPresenceAhead, flexGrow needs flexBasis, no stray absolute positioning).
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
import { View } from "@react-pdf/renderer";
|
|
import { getMeasurer, measureToken } from "../engine/measure";
|
|
import { resolveFace, roleToken, type RoleName, type TypeToken } from "../fonts/roles";
|
|
import { chunkIdentifier } from "../model/text";
|
|
import { useAvailableWidth } from "./AvailableWidth";
|
|
import { fitSize, type FitInfo } from "./FitText";
|
|
import { Txt, type TxtProps } from "./Txt";
|
|
|
|
export interface IdTextInfo extends FitInfo {
|
|
mode: "single" | "chunked";
|
|
chunks: string[];
|
|
}
|
|
|
|
export interface IdTextProps extends Omit<TxtProps, "children" | "role" | "maxLines" | "width" | "grow"> {
|
|
role: RoleName | TypeToken;
|
|
children: string;
|
|
/** Width to fit; defaults to the available width of the enclosing cell. */
|
|
width?: number;
|
|
minSize?: number;
|
|
/** For tests and audits only. */
|
|
onFit?: (info: IdTextInfo) => void;
|
|
}
|
|
|
|
/**
|
|
* Text for identifiers (e-mail, URL, GSTIN, phone). textkit never breaks inside a word, so a long
|
|
* value would overflow silently. This shrinks it to the role's minimum size; if it is still too wide
|
|
* it is split at "@ . / - _" into a flex-wrap row of adjacent Text chunks (no gaps, so extraction
|
|
* joins them back into the original string).
|
|
*/
|
|
export function IdText({ role, children, width, minSize, onFit, ...rest }: IdTextProps) {
|
|
const avail = useAvailableWidth();
|
|
const target = width ?? avail;
|
|
const token = roleToken(role);
|
|
const fit = fitSize(token, children, target, minSize, { weight: rest.weight, italic: rest.italic });
|
|
|
|
if (fit.fits) {
|
|
onFit?.({ ...fit, mode: "single", chunks: [children] });
|
|
return <Txt {...rest} role={{ ...token, size: fit.size }}>{children}</Txt>;
|
|
}
|
|
|
|
const sized: TypeToken = { ...token, size: fit.size };
|
|
const m = getMeasurer();
|
|
const family = resolveFace(token.face, rest.faceOverrides);
|
|
const widthOf = (s: string) =>
|
|
measureToken(m, sized, s, { weight: rest.weight, italic: rest.italic, family });
|
|
let chunks = chunkIdentifier(children, 1);
|
|
for (let max = children.length; max >= 1; max--) {
|
|
const candidate = chunkIdentifier(children, max);
|
|
if (candidate.every((c) => widthOf(c) <= target + 1e-6)) {
|
|
chunks = candidate;
|
|
break;
|
|
}
|
|
}
|
|
onFit?.({ ...fit, mode: "chunked", chunks });
|
|
return (
|
|
<View style={{ flexDirection: "row", flexWrap: "wrap", width: target, minWidth: target, maxWidth: target } as never}>
|
|
{chunks.map((c, i) => (
|
|
<Txt key={i} {...rest} align={undefined} role={sized}>
|
|
{c}
|
|
</Txt>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|