communication-react-19
Version:
React library for building modern communication user experiences utilizing Azure Communication Services (React 19 compatible fork)
187 lines • 9.15 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { mergeStyles } from '@fluentui/react';
import React, { useRef, useEffect, useState, useMemo } from 'react';
import { gridLayoutStyle } from './styles/GridLayout.styles';
/**
* A component to lay out audio / video participants tiles in a call.
*
* @public
*/
export const GridLayout = (props) => {
const { children, styles } = props;
const numberOfChildren = React.Children.count(children);
const [currentWidth, setCurrentWidth] = useState(0);
const [currentHeight, setCurrentHeight] = useState(0);
const containerRef = useRef(null);
const observer = useRef(new ResizeObserver((entries) => {
if (!entries[0]) {
return;
}
const { width, height } = entries[0].contentRect;
setCurrentWidth(width);
setCurrentHeight(height);
}));
useEffect(() => {
if (containerRef.current) {
observer.current.observe(containerRef.current);
}
const currentObserver = observer.current;
return () => currentObserver.disconnect();
}, [observer, containerRef]);
const gridProps = useMemo(() => {
return calculateGridProps(numberOfChildren, currentWidth, currentHeight);
}, [numberOfChildren, currentWidth, currentHeight]);
const cssGridStyles = useMemo(() => createGridStyles(numberOfChildren, gridProps), [numberOfChildren, gridProps]);
return (React.createElement("div", { ref: containerRef, className: mergeStyles(gridLayoutStyle, cssGridStyles, styles === null || styles === void 0 ? void 0 : styles.root), "data-ui-id": "grid-layout" }, children));
};
/**
* The cell aspect ratio we aim for in a grid
*/
const TARGET_CELL_ASPECT_RATIO = 16 / 9;
/**
* The minimum cell aspect ratio we allow
*/
const MINIMUM_CELL_ASPECT_RATIO_ALLOWED = 8 / 9;
const isCloserThan = (a, b, target) => {
return Math.abs(target - a) < Math.abs(target - b);
};
/**
* Get the best GridProps to place a number of items in a grid as evenly as possible given the width and height of the grid
* @param numberOfItems - number of items to place in grid
* @param width - width of grid
* @param height - height of grid
* @returns GridProps
*/
export const calculateGridProps = (numberOfItems, width, height) => {
if (numberOfItems <= 0) {
return { fillDirection: 'horizontal', rows: 0, columns: 0 };
}
// If width or height are 0 then we return rows and column evenly
if (width <= 0 || height <= 0) {
return {
fillDirection: 'horizontal',
rows: Math.ceil(Math.sqrt(numberOfItems)),
columns: Math.ceil(Math.sqrt(numberOfItems))
};
}
const aspectRatio = width / height;
// Approximate how many rows to divide the grid to achieve cells close to the TARGET_CELL_ASPECT_RATIO
let rows = Math.floor(Math.sqrt((TARGET_CELL_ASPECT_RATIO / aspectRatio) * numberOfItems)) || 1;
// Make sure rows do not exceed numberOfItems
rows = Math.min(rows, numberOfItems);
// Given the rows, get the minimum columns needed to create enough cells for the number of items
let columns = Math.ceil(numberOfItems / rows);
// Default fill direction to horizontal
let fillDirection = 'horizontal';
while (rows < numberOfItems) {
// If cell aspect ratio is less than MINIMUM_CELL_ASPECT_RATIO_ALLOWED then try more rows
if ((rows / columns) * aspectRatio < MINIMUM_CELL_ASPECT_RATIO_ALLOWED) {
rows += 1;
columns = Math.ceil(numberOfItems / rows);
continue;
}
if (numberOfItems < rows * columns) {
// We need to check that stretching columns vertically will result in only one less cell in stretched columns.
// Likewise, we need to check that stretching rows horizonally will result in only one less cell in stretched rows.
// e.g. For 4 rows, 2 columns, but only 6 items, we cannot stretch vertically because that would result in a
// column of 2 cells which is less by more than 1 compared to the unstretched column.
// _________
// |____| |
// |____|____|
// |____| |
// |____|____|
const canStretchVertically = numberOfItems >= rows + (columns - 1) * (rows - 1);
const canStretchHorizontally = numberOfItems >= columns + (rows - 1) * (columns - 1);
if (!canStretchVertically && !canStretchHorizontally) {
rows += 1;
columns = Math.ceil(numberOfItems / rows);
continue;
}
else if (!canStretchVertically) {
break;
}
else if (!canStretchHorizontally) {
fillDirection = 'vertical';
break;
}
// We need to figure out whether the big cells should stretch horizontally or vertically
// to fill in the empty spaces
// e.g. For 2 rows, 3 columns, but only 5 items, we need to choose whether to stretch cells
// horizontally or vertically
// ______________________ _______________________
// | | | | | | | |
// |_______|_______|______| |_______|_______| |
// | | | | | | |
// |___________|__________| |_______|_______|_______|
// Calculate the aspect ratio of big cells stretched horizontally
const horizontallyStretchedCellRatio = (rows / (columns - 1)) * aspectRatio;
// Calculate the aspect ratio of big cells stretched vertically
const verticallyStretchedCellRatio = ((rows - 1) / columns) * aspectRatio;
// We know the horizontally stretched cells aspect ratio is higher than MINIMUM_CELL_ASPECT_RATIO_ALLOWED. If vertically stretched cells
// is also higher than the MINIMUM_CELL_ASPECT_RATIO_ALLOWED, then choose which aspect ratio is better.
if (verticallyStretchedCellRatio >= MINIMUM_CELL_ASPECT_RATIO_ALLOWED) {
// If vertically stetched cell has an aspect ratio closer to TARGET_CELL_ASPECT_RATIO then change the fill direction to vertical
if (isCloserThan(verticallyStretchedCellRatio, horizontallyStretchedCellRatio, TARGET_CELL_ASPECT_RATIO)) {
fillDirection = 'vertical';
}
}
}
break;
}
return { fillDirection, rows, columns };
};
/**
* Creates a styles classname with CSS Grid related styles given GridProps and the number of items to distribute as evenly as possible.
* @param numberOfItems - number of items to place in grid
* @param gridProps - GridProps that define the number of rows, number of columns, and the fill direction
* @returns - classname
*/
export const createGridStyles = (numberOfItems, gridProps) => {
const isHorizontal = gridProps.fillDirection === 'horizontal';
// Blocks are either rows or columns depending on whether we fill horizontally or vertically. Each block may differ in the number of cells.
const blocks = isHorizontal ? gridProps.rows : gridProps.columns;
const smallCellsPerBlock = Math.ceil(numberOfItems / blocks);
const bigCellsPerBlock = Math.floor(numberOfItems / blocks);
const numBigCells = (gridProps.rows * gridProps.columns - numberOfItems) * bigCellsPerBlock;
// Get grid units
// e.g. If some blocks have 2 big cells while others have 3 small cells, we need to work with 6 units per block
const units = smallCellsPerBlock * bigCellsPerBlock;
const gridStyles = isHorizontal
? {
gridTemplateColumns: `repeat(${units}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${blocks}, minmax(0, 1fr))`,
gridAutoFlow: 'row'
}
: {
gridTemplateColumns: `repeat(${blocks}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${units}, minmax(0, 1fr))`,
gridAutoFlow: 'column'
};
const smallCellStyle = isHorizontal
? {
'> *': {
gridColumn: `auto / span ${units / smallCellsPerBlock}`
}
}
: {
'> *': {
gridRow: `auto / span ${units / smallCellsPerBlock}`
}
};
// If there are big cells, we are choosing to place the latest children into the big cells.
// That is why we use the '> *:nth-last-child(-n + ${numBigCells})' CSS selector below
const bigCellStyle = numBigCells
? {
[`> *:nth-last-child(-n + ${numBigCells})`]: isHorizontal
? {
gridColumn: `auto / span ${units / bigCellsPerBlock}`
}
: {
gridRow: `auto / span ${units / bigCellsPerBlock}`
}
}
: {};
return mergeStyles(gridStyles, smallCellStyle, bigCellStyle);
};
//# sourceMappingURL=GridLayout.js.map