Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/

# Vitest test attachments
.vitest-attachments/

# OS generated files
.DS_Store
Thumbs.db
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ export type AuthType = 'signin' | 'signup' | 'recovery';
/**
* Get the appropriate FieldType for an input component.
*/
/** Upper bound on STACK grid slots, so a mistyped `items` cannot render thousands of cells. */
const MAX_STACK_ITEMS = 12;

const getFieldType = (variant: EmbeddedFlowComponentType): FieldType => {
switch (variant) {
case EmbeddedFlowComponentType.EmailInput:
Expand Down Expand Up @@ -726,14 +729,48 @@ const createAuthComponentFromFlow = (
const align: string = (component as any).align || 'center';
const justify: string = (component as any).justify || 'flex-start';

const stackStyle: CSSProperties = {
alignItems: align,
display: 'flex',
flexDirection: direction as CSSProperties['flexDirection'],
flexWrap: 'wrap',
gap: `${gap * 0.5}rem`,
justifyContent: justify,
};
// `items` is the number of slots across the main axis and `direction` picks
// that axis: with `row` it is the column count (items: 2 with four buttons
// renders a 2 x 2 grid), and with `column` it is the row count and children
// flow into further columns. Fewer than two slots keeps the flex layout, so
// stacks authored before grid support are unaffected.
const rawItems: string | number | undefined = component.items;
const parsedItems: number =
typeof rawItems === 'string' ? Number(rawItems) : typeof rawItems === 'number' ? rawItems : NaN;
const items: number | null =
Number.isFinite(parsedItems) && Math.floor(parsedItems) >= 2
? Math.min(Math.floor(parsedItems), MAX_STACK_ITEMS)
: null;
Comment thread
DonOmalVindula marked this conversation as resolved.
// CSS Grid has no reverse auto-flow, so the reverse variants fall back to
// their base axis rather than silently becoming a row.
const isColumn: boolean = direction.startsWith('column');
const gridJustify: string = component.justify ?? 'stretch';
// Equal `1fr` tracks fill the container, leaving `justify-content` no free
// space to distribute. Content-sized tracks restore it, so every justify
// value has a visible effect while the default stays an even split.
const track: string = gridJustify === 'stretch' ? '1fr' : 'auto';

const stackStyle: CSSProperties =
items !== null
? {
alignItems: component.align ?? 'stretch',
display: 'grid',
gap: `${gap * 0.5}rem`,
gridAutoFlow: isColumn ? 'column' : 'row',
justifyContent: gridJustify,
width: '100%',
...(isColumn
? {gridTemplateRows: `repeat(${items}, ${track})`}
: {gridTemplateColumns: `repeat(${items}, ${track})`}),
}
: {
alignItems: align,
display: 'flex',
flexDirection: direction as CSSProperties['flexDirection'],
flexWrap: 'wrap',
gap: `${gap * 0.5}rem`,
justifyContent: justify,
};

const stackChildren: (ReactElement | null)[] = component.components
? component.components.map((childComponent: any, index: number) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,96 @@ describe('AuthOptionFactory rich-text action', () => {
expect(() => container.querySelector<HTMLAnchorElement>('a')!.click()).not.toThrow();
});
});

describe('AuthOptionFactory stack grid layout', () => {
const stackWith = (extra: Record<string, unknown>, childCount = 4): EmbeddedFlowComponent =>
({
components: Array.from({length: childCount}, (_, index) => ({
id: `text_${index}`,
label: `<p>Option ${index}</p>`,
type: EmbeddedFlowComponentType.RichText,
})),
id: 'stack_1',
type: EmbeddedFlowComponentType.Stack,
...extra,
}) as EmbeddedFlowComponent;

const stackElement = (component: EmbeddedFlowComponent): HTMLElement => {
const {container} = renderInto(component);
return container.querySelector<HTMLElement>('#stack_1')!;
};

it('renders a grid with the configured number of columns when items is 2', () => {
const stack = stackElement(stackWith({items: 2}));

expect(stack.style.display).toBe('grid');
expect(stack.style.gridTemplateColumns).toBe('repeat(2, 1fr)');
expect(stack.children).toHaveLength(4);
});

it('supports arbitrary column counts provided as a numeric string', () => {
const stack = stackElement(stackWith({items: '3'}, 5));

expect(stack.style.display).toBe('grid');
expect(stack.style.gridTemplateColumns).toBe('repeat(3, 1fr)');
expect(stack.children).toHaveLength(5);
});

it('keeps the flex layout when items is absent', () => {
const stack = stackElement(stackWith({direction: 'column'}));

expect(stack.style.display).toBe('flex');
expect(stack.style.flexDirection).toBe('column');
});

it('keeps the flex layout when items is 1', () => {
// Stacks authored in the flow builder are seeded with items 1, so a single slot
// must not promote them to a grid.
const stack = stackElement(stackWith({items: 1}));

expect(stack.style.display).toBe('flex');
});

it('falls back to the base axis for the reverse directions', () => {
expect(stackElement(stackWith({direction: 'column-reverse', items: 2})).style.gridAutoFlow).toBe('column');
expect(stackElement(stackWith({direction: 'row-reverse', items: 2})).style.gridAutoFlow).toBe('row');
});

it('uses content-sized tracks when justify has to distribute free space', () => {
const stack = stackElement(stackWith({items: 2, justify: 'space-between'}));

expect(stack.style.gridTemplateColumns).toBe('repeat(2, auto)');
expect(stack.style.justifyContent).toBe('space-between');
});

it('keeps equal tracks when justify is unset', () => {
expect(stackElement(stackWith({items: 2})).style.gridTemplateColumns).toBe('repeat(2, 1fr)');
});

it('clamps absurd slot counts', () => {
const stack = stackElement(stackWith({items: 5000}));

expect(stack.style.gridTemplateColumns).toBe('repeat(12, 1fr)');
});

it('treats items as the row count when direction is column', () => {
const stack = stackElement(stackWith({direction: 'column', items: 2}));

expect(stack.style.display).toBe('grid');
expect(stack.style.gridTemplateRows).toBe('repeat(2, 1fr)');
expect(stack.style.gridAutoFlow).toBe('column');
expect(stack.style.gridTemplateColumns).toBe('');
});

it('falls back to the flex layout when items is not numeric', () => {
const stack = stackElement(stackWith({items: 'garbage'}));

expect(stack.style.display).toBe('flex');
});

it('falls back to the flex layout when items is a malformed numeric string', () => {
const stack = stackElement(stackWith({items: '2invalid'}));

expect(stack.style.display).toBe('flex');
});
});
Loading