Signal-Desktop/ts/components/fun/base/FunGrid.tsx

269 lines
5.6 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CSSProperties, ReactNode } from 'react';
import React from 'react';
import { FocusScope } from 'react-aria';
import classNames from 'classnames';
import { Button, Dialog, Header, Popover } from 'react-aria-components';
import { FunScrollerSection } from './FunScroller';
/**
* Grid Container
*/
export type FunGridContainerProps = Readonly<{
totalSize: number;
columnCount: number;
cellWidth: number;
cellHeight: number;
children: ReactNode;
}>;
export function FunGridContainer(props: FunGridContainerProps): JSX.Element {
return (
<FocusScope restoreFocus>
<div
className="FunGrid__Container"
style={
{
'--fun-grid-container-total-size': `${props.totalSize}px`,
'--fun-grid-container-column-count': props.columnCount,
'--fun-grid-container-cell-width': `${props.cellWidth}px`,
'--fun-grid-container-cell-height': `${props.cellHeight}px`,
} as CSSProperties
}
>
{props.children}
</div>
</FocusScope>
);
}
/**
* Grid Section
*/
export type FunGridScrollerSectionProps = Readonly<{
id: string;
sectionOffset: number;
sectionSize: number;
children: ReactNode;
}>;
export function FunGridScrollerSection(
props: FunGridScrollerSectionProps
): JSX.Element {
return (
<FunScrollerSection
id={props.id}
className="FunGrid__ScrollerSection"
style={
{
'--fun-grid-scroller-section-offset': `${props.sectionOffset}px`,
'--fun-grid-scroller-section-size': `${props.sectionSize}px`,
} as CSSProperties
}
>
{props.children}
</FunScrollerSection>
);
}
/**
* Grid Header
*/
export type FunGridHeaderProps = Readonly<{
id: string;
headerOffset: number;
headerSize: number;
children: ReactNode;
}>;
export function FunGridHeader(props: FunGridHeaderProps): JSX.Element {
return (
<h3
id={props.id}
className="FunGrid__Header"
style={
{
'--fun-grid-header-offset': `${props.headerOffset}px`,
'--fun-grid-header-size': `${props.headerSize}px`,
} as CSSProperties
}
>
{props.children}
</h3>
);
}
/**
* Grid Header Text
*/
export type FunGridHeaderTextProps = Readonly<{
children: ReactNode;
}>;
export function FunGridHeaderText(props: FunGridHeaderTextProps): JSX.Element {
return <span className="FunGrid__HeaderText">{props.children}</span>;
}
/**
* Grid Header Button
*/
export type FunGridHeaderButtonProps = Readonly<{
label: string;
onPress?: () => void;
children: ReactNode;
}>;
export function FunGridHeaderButton(
props: FunGridHeaderButtonProps
): JSX.Element {
return (
<Button
type="button"
aria-label={props.label}
className="FunGrid__HeaderButton"
onPress={props.onPress}
>
{props.children}
</Button>
);
}
/**
* Grid Header Button
*/
export type FunGridHeaderIconProps = Readonly<{
iconClassName: `FunGrid__HeaderIcon--${string}`;
}>;
export function FunGridHeaderIcon(props: FunGridHeaderIconProps): JSX.Element {
return (
<div className={classNames('FunGrid__HeaderIcon', props.iconClassName)} />
);
}
/**
* Grid Header Popover
*/
export type FunGridHeaderPopoverProps = Readonly<{
children: ReactNode;
}>;
export function FunGridHeaderPopover(
props: FunGridHeaderPopoverProps
): JSX.Element {
return (
<Popover
data-fun-overlay
className="FunGrid__HeaderPopover"
placement="bottom end"
offset={6}
>
<Dialog className="FunGrid__HeaderPopoverDialog">{props.children}</Dialog>
</Popover>
);
}
export type FunGridHeaderPopoverTextProps = Readonly<{
children: ReactNode;
}>;
export function FunGridHeaderPopoverHeader(
props: FunGridHeaderPopoverTextProps
): JSX.Element {
return (
<Header className="FunGrid__HeaderPopoverHeader">{props.children}</Header>
);
}
/**
* Grid Row Group
*/
export type FunGridRowGroupProps = Readonly<{
'aria-labelledby': string;
colCount: number;
rowCount: number;
rowGroupOffset: number;
rowGroupSize: number;
children: ReactNode;
}>;
export function FunGridRowGroup(props: FunGridRowGroupProps): JSX.Element {
return (
<div
role="grid"
aria-colcount={props.colCount}
aria-rowcount={props.rowCount}
aria-labelledby={props['aria-labelledby']}
className="FunGrid__RowGroup"
style={
{
'--fun-grid-row-group-offset': `${props.rowGroupOffset}px`,
'--fun-grid-row-group-size': `${props.rowGroupSize}px`,
} as CSSProperties
}
>
{props.children}
</div>
);
}
/**
* Grid Row
*/
export type FunGridRowProps = Readonly<{
rowIndex: number;
children: ReactNode;
}>;
export function FunGridRow(props: FunGridRowProps): JSX.Element {
return (
<div
role="row"
className="FunGrid__Row"
aria-rowindex={props.rowIndex + 1}
style={
{
'--fun-grid-row-index': props.rowIndex + 1,
} as CSSProperties
}
>
{props.children}
</div>
);
}
/**
* Grid Cell
*/
export type FunGridCellProps = Readonly<{
'data-key': string;
rowIndex: number;
colIndex: number;
children: ReactNode;
}>;
export function FunGridCell(props: FunGridCellProps): JSX.Element {
return (
<div
data-key={props['data-key']}
role="gridcell"
className="FunGrid__Cell"
aria-rowindex={props.rowIndex + 1}
aria-colindex={props.colIndex + 1}
>
{props.children}
</div>
);
}