fix: improve field sizing (#1486)

## Description

Allows for smaller field sizing in addition to improving our styling
when displaying labels on smaller fields.

This is the minimum currently supported field size until we perform a
more extensive refactor of our current drag and drop system.

## Related Issue

Reported via support channels

## Changes Made

- Updated our minimum size constraints
- Attempted to add a general autosizing component for text and failed
- Updated styling in a bunch of places to use the css `clamp()` method
for dynamic sizing.
This commit is contained in:
Lucas Smith
2024-11-20 22:49:30 +11:00
committed by GitHub
parent 9ef8b1f0c3
commit 83e7a3c222
26 changed files with 448 additions and 262 deletions

View File

@ -0,0 +1,157 @@
'use client';
import { useLayoutEffect, useRef } from 'react';
import { cn } from '../lib/utils';
export type Dimensions = {
height: number;
width: number;
};
export type AutoSizedTextProps = {
children: React.ReactNode;
className?: string;
maxHeight?: number;
useRem?: boolean;
};
const ITERATION_LIMIT = 20;
const MAXIMUM_DIFFERENCE = 1; // px
function getElementDimensions(element: HTMLElement): Dimensions {
const bbox = element.getBoundingClientRect();
return {
width: bbox.width,
height: bbox.height,
};
}
function getBaseFontSize(): number {
try {
const fontSize = getComputedStyle(document.documentElement).fontSize;
const parsed = parseFloat(fontSize);
// Check if we got a valid number
if (!Number.isFinite(parsed)) {
return 16;
}
return parsed;
} catch (error) {
// Fallback to browser default if anything goes wrong
return 16;
}
}
function pxToRem(px: number): number {
return px / getBaseFontSize();
}
export function AutoSizedText({
children,
className,
maxHeight,
useRem = false,
}: AutoSizedTextProps) {
const childRef = useRef<HTMLDivElement>(null);
const fontSize = useRef<number>(0);
const fontSizeLowerBound = useRef<number>(0);
const fontSizeUpperBound = useRef<number>(0);
const adjustFontSize = (childDimensions: Dimensions, parentDimensions: Dimensions) => {
const childElement = childRef.current;
if (!childElement) {
return;
}
let newFontSize: number;
const targetHeight =
maxHeight && maxHeight < parentDimensions.height ? maxHeight : parentDimensions.height;
const isElementTooBig =
childDimensions.width > parentDimensions.width || childDimensions.height > targetHeight;
if (isElementTooBig) {
// Scale down if element is bigger than target
newFontSize = (fontSizeLowerBound.current + fontSize.current) / 2;
fontSizeUpperBound.current = fontSize.current;
} else if (
childDimensions.width < parentDimensions.width ||
childDimensions.height < parentDimensions.height
) {
// Scale up if element is smaller than target
newFontSize = (fontSizeUpperBound.current + fontSize.current) / 2;
fontSizeLowerBound.current = fontSize.current;
}
fontSize.current = newFontSize;
// Convert to rem if useRem is true
const displayFontSize = useRem ? `${pxToRem(newFontSize)}rem` : `${newFontSize}px`;
childElement.style.fontSize = displayFontSize;
};
useLayoutEffect(() => {
const childElement = childRef.current;
const parentElement = childRef.current?.parentElement;
if (!childElement || !parentElement) {
return;
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry) {
return;
}
const parentDimensions = entry.contentRect;
// Reset iteration parameters
fontSizeLowerBound.current = 0;
fontSizeUpperBound.current = parentDimensions.height;
let iterationCount = 0;
while (iterationCount <= ITERATION_LIMIT) {
const childDimensions = getElementDimensions(childElement);
const targetHeight =
maxHeight && maxHeight < parentDimensions.height ? maxHeight : parentDimensions.height;
const widthDifference = parentDimensions.width - childDimensions.width;
const heightDifference = targetHeight - childDimensions.height;
const childFitsIntoParent = heightDifference >= 0 && widthDifference >= 0;
const isWithinTolerance =
Math.abs(widthDifference) <= MAXIMUM_DIFFERENCE ||
Math.abs(heightDifference) <= MAXIMUM_DIFFERENCE;
if (childFitsIntoParent && isWithinTolerance) {
break;
}
adjustFontSize(childDimensions, parentDimensions);
iterationCount += 1;
}
});
observer.observe(parentElement);
return () => {
observer.disconnect();
};
}, [maxHeight, useRem]);
return (
<div ref={childRef} className={cn('inline-block leading-none', className)}>
{children}
</div>
);
}

View File

@ -76,8 +76,11 @@ const fontCaveat = Caveat({
variable: '--font-caveat',
});
const MIN_HEIGHT_PX = 20;
const MIN_WIDTH_PX = 80;
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5;
const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type FieldFormType = {
nativeId?: number;
@ -480,8 +483,8 @@ export const AddFieldsFormPartial = ({
}
fieldBounds.current = {
height: Math.max(MIN_HEIGHT_PX),
width: Math.max(MIN_WIDTH_PX),
height: Math.max(DEFAULT_HEIGHT_PX),
width: Math.max(DEFAULT_WIDTH_PX),
};
});
@ -594,7 +597,7 @@ export const AddFieldsFormPartial = ({
{selectedField && (
<div
className={cn(
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
selectedSignerStyles.default.base,
{
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
@ -609,7 +612,9 @@ export const AddFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
<span className="text-[clamp(0.425rem,25cqw,0.825rem)]">
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
</span>
</div>
)}
@ -630,8 +635,10 @@ export const AddFieldsFormPartial = ({
selectedSigner?.email !== field.signerEmail ||
!canRecipientBeModified(selectedSigner, fields)
}
minHeight={fieldBounds.current.height}
minWidth={fieldBounds.current.width}
minHeight={MIN_HEIGHT_PX}
minWidth={MIN_WIDTH_PX}
defaultHeight={DEFAULT_HEIGHT_PX}
defaultWidth={DEFAULT_WIDTH_PX}
passive={isFieldWithinBounds && !!selectedField}
onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)}

View File

@ -45,7 +45,7 @@ export const FieldIcon = ({
return (
<div
className={cn(
'text-field-card-foreground flex items-center justify-center gap-x-1 text-[clamp(0.875rem,1.8cqw,1.2rem)]',
'text-field-card-foreground flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
fontCaveatClassName,
)}
>
@ -71,8 +71,9 @@ export const FieldIcon = ({
}
return (
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.625rem,1cqw,0.825rem)]">
<Icon className="h-4 w-4" /> {label}
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.425rem,25cqw,0.825rem)]">
<Icon className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />{' '}
{label}
</div>
);
}

View File

@ -35,6 +35,8 @@ export type FieldItemProps = {
disabled?: boolean;
minHeight?: number;
minWidth?: number;
defaultHeight?: number;
defaultWidth?: number;
onResize?: (_node: HTMLElement) => void;
onMove?: (_node: HTMLElement) => void;
onRemove?: () => void;
@ -53,6 +55,8 @@ export const FieldItem = ({
disabled,
minHeight,
minWidth,
defaultHeight,
defaultWidth,
onResize,
onMove,
onRemove,
@ -68,8 +72,8 @@ export const FieldItem = ({
const [coords, setCoords] = useState({
pageX: 0,
pageY: 0,
pageHeight: 0,
pageWidth: 0,
pageHeight: defaultHeight || 0,
pageWidth: defaultWidth || 0,
});
const [settingsActive, setSettingsActive] = useState(false);
const $el = useRef(null);

View File

@ -44,18 +44,18 @@ export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
width: `${coords.width}px`,
}}
>
<Card className={cn('bg-background h-full w-full')}>
<Card className={cn('bg-background h-full w-full [container-type:size]')}>
<CardContent
className={cn(
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-2 text-xl',
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-0 text-[clamp(0.575rem,1.8cqw,1.2rem)] leading-none',
field.type === FieldType.SIGNATURE && fontCaveat.className,
)}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
<p className="text-muted-foreground/50 w-full truncate text-center text-xs">
{/* <p className="text-muted-foreground/50 w-full truncate text-center text-xs">
{signerEmail}
</p>
</p> */}
</CardContent>
</Card>
</div>,

View File

@ -67,8 +67,11 @@ const fontCaveat = Caveat({
variable: '--font-caveat',
});
const MIN_HEIGHT_PX = 20;
const MIN_WIDTH_PX = 80;
const MIN_HEIGHT_PX = 12;
const MIN_WIDTH_PX = 36;
const DEFAULT_HEIGHT_PX = MIN_HEIGHT_PX * 2.5;
const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type AddTemplateFieldsFormProps = {
documentFlow: DocumentFlowStep;
@ -354,8 +357,8 @@ export const AddTemplateFieldsFormPartial = ({
}
fieldBounds.current = {
height: Math.max(MIN_HEIGHT_PX),
width: Math.max(MIN_WIDTH_PX),
height: Math.max(DEFAULT_HEIGHT_PX),
width: Math.max(DEFAULT_WIDTH_PX),
};
});
@ -425,7 +428,7 @@ export const AddTemplateFieldsFormPartial = ({
{selectedField && (
<div
className={cn(
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]',
selectedSignerStyles.default.base,
{
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
@ -439,7 +442,9 @@ export const AddTemplateFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
<span className="text-[clamp(0.425rem,25cqw,0.825rem)]">
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[selectedField])}
</span>
</div>
)}
@ -452,8 +457,10 @@ export const AddTemplateFieldsFormPartial = ({
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
field={field}
disabled={selectedSigner?.email !== field.signerEmail}
minHeight={fieldBounds.current.height}
minWidth={fieldBounds.current.width}
minHeight={MIN_HEIGHT_PX}
minWidth={MIN_WIDTH_PX}
defaultHeight={DEFAULT_HEIGHT_PX}
defaultWidth={DEFAULT_WIDTH_PX}
passive={isFieldWithinBounds && !!selectedField}
onResize={(options) => onFieldResize(options, index)}
onMove={(options) => onFieldMove(options, index)}