Ensure all components are React.memo()-ed (#8815)

* Ensure all components are React.memo()-ed

* Align composite creation

* Adjust

* Add Td colSpan & rowSpan attributes
This commit is contained in:
Jaco 2023-01-16 08:36:22 +02:00 committed by GitHub
parent af73c998ad
commit 4ed0167f6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 539 additions and 550 deletions

View File

@ -1,158 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ButtonProps } from './types';
import React, { useCallback, useEffect } from 'react';
import styled from 'styled-components';
import Icon from '../Icon';
import Spinner from '../Spinner';
function Button ({ activeOnEnter, children, className = '', dataTestId = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, isSelected, isToplevel, label, onClick, isReadOnly = !onClick, onMouseEnter, onMouseLeave, tabIndex, withoutLink }: ButtonProps): React.ReactElement<ButtonProps> {
const _onClick = useCallback(
(): void => {
!(isBusy || isDisabled) && onClick && onClick();
},
[isBusy, isDisabled, onClick]
);
const _onMouseEnter = useCallback((): void => {
onMouseEnter && onMouseEnter();
}, [onMouseEnter]);
const _onMouseLeave = useCallback((): void => {
onMouseLeave && onMouseLeave();
}, [onMouseLeave]);
const listenKeyboard = useCallback((event: KeyboardEvent): void => {
if (!isBusy && !isDisabled && event.key === 'Enter') {
onClick && onClick();
}
}, [isBusy, isDisabled, onClick]);
useEffect(() => {
if (activeOnEnter) {
window.addEventListener('keydown', listenKeyboard, true);
}
return () => {
if (activeOnEnter) {
window.removeEventListener('keydown', listenKeyboard, true);
}
};
}, [activeOnEnter, listenKeyboard]);
return (
<StyledButton
className={`${className} ui--Button ${label ? 'hasLabel' : ''} ${isBasic ? 'isBasic' : ''} ${isCircular ? 'isCircular' : ''} ${isFull ? 'isFull' : ''} ${isIcon ? 'isIcon' : ''} ${(isBusy || isDisabled) ? 'isDisabled' : ''} ${isBusy ? 'isBusy' : ''} ${isReadOnly ? 'isReadOnly' : ''}${isSelected ? 'isSelected' : ''} ${isToplevel ? 'isToplevel' : ''} ${withoutLink ? 'withoutLink' : ''}`}
data-testid={dataTestId}
onClick={_onClick}
onMouseEnter={_onMouseEnter}
onMouseLeave={_onMouseLeave}
tabIndex={tabIndex}
>
{icon && <Icon icon={icon} />}
{label}
{children}
{isBusy && (
<Spinner
className='ui--Button-spinner'
variant='cover'
/>
)}
</StyledButton>
);
}
const ICON_PADDING = 0.5;
const StyledButton = styled.button`
background: transparent;
border: none;
color: inherit;
cursor: pointer;
line-height: 1;
margin: 0;
outline: none;
position: relative;
vertical-align: middle;
text-align: center;
&:not(.hasLabel) {
padding: 0.7em;
.ui--Icon {
padding: 0.6rem;
margin: -0.6rem;
}
}
&:not(.isCircular) {
border-radius: 0.25rem;
}
&:focus {
outline:0;
}
&.hasLabel {
padding: 0.7rem 1.1rem 0.7rem ${1.1 - ICON_PADDING}rem;
.ui--Icon {
margin-right: 0.425rem !important;
}
}
&.isBasic {
background: var(--bg-table);
}
&.isCircular {
border-radius: 10rem;
}
&.isDisabled, &.isReadOnly {
background: none;
box-shadow: none;
cursor: not-allowed;
}
&.isBusy {
cursor: wait;
}
&.isFull {
display: block;
width: 100%;
}
&.isIcon {
background: transparent;
}
.ui--Button-overlay {
background: rgba(253, 252, 251, 0.75);
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
visibility: hidden;
}
.ui--Icon {
border-radius: 50%;
box-sizing: content-box;
height: 1rem;
margin: -${ICON_PADDING}rem 0;
padding: ${ICON_PADDING}rem;
width: 1rem;
}
&.isDisabled {
color: #bcbbba;
}
`;
export default React.memo(Button);

View File

@ -1,12 +1,16 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { GroupProps } from './types';
import React from 'react';
import styled from 'styled-components';
function ButtonGroup ({ children, className = '', isCentered }: GroupProps): React.ReactElement<GroupProps> {
interface Props {
children?: React.ReactNode;
className?: string;
isCentered?: boolean;
}
function ButtonGroup ({ children, className = '', isCentered }: Props): React.ReactElement<Props> {
return (
<StyledDiv className={`${className} ui--Button-Group ${isCentered ? 'isCentered' : ''}`}>
{children}

View File

@ -1,12 +1,164 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ButtonType } from './types';
import type { ButtonProps as Props } from './types';
import IButton from './Button';
import React, { useCallback, useEffect } from 'react';
import styled from 'styled-components';
import Icon from '../Icon';
import Spinner from '../Spinner';
import Group from './Group';
const Button = IButton as unknown as ButtonType;
function ButtonBase ({ activeOnEnter, children, className = '', dataTestId = '', icon, isBasic, isBusy, isCircular, isDisabled, isFull, isIcon, isSelected, isToplevel, label, onClick, isReadOnly = !onClick, onMouseEnter, onMouseLeave, tabIndex, withoutLink }: Props): React.ReactElement<Props> {
const _onClick = useCallback(
(): void => {
!(isBusy || isDisabled) && onClick && onClick();
},
[isBusy, isDisabled, onClick]
);
const _onMouseEnter = useCallback((): void => {
onMouseEnter && onMouseEnter();
}, [onMouseEnter]);
const _onMouseLeave = useCallback((): void => {
onMouseLeave && onMouseLeave();
}, [onMouseLeave]);
const listenKeyboard = useCallback((event: KeyboardEvent): void => {
if (!isBusy && !isDisabled && event.key === 'Enter') {
onClick && onClick();
}
}, [isBusy, isDisabled, onClick]);
useEffect(() => {
if (activeOnEnter) {
window.addEventListener('keydown', listenKeyboard, true);
}
return () => {
if (activeOnEnter) {
window.removeEventListener('keydown', listenKeyboard, true);
}
};
}, [activeOnEnter, listenKeyboard]);
return (
<StyledButton
className={`${className} ui--Button ${label ? 'hasLabel' : ''} ${isBasic ? 'isBasic' : ''} ${isCircular ? 'isCircular' : ''} ${isFull ? 'isFull' : ''} ${isIcon ? 'isIcon' : ''} ${(isBusy || isDisabled) ? 'isDisabled' : ''} ${isBusy ? 'isBusy' : ''} ${isReadOnly ? 'isReadOnly' : ''}${isSelected ? 'isSelected' : ''} ${isToplevel ? 'isToplevel' : ''} ${withoutLink ? 'withoutLink' : ''}`}
data-testid={dataTestId}
onClick={_onClick}
onMouseEnter={_onMouseEnter}
onMouseLeave={_onMouseLeave}
tabIndex={tabIndex}
>
{icon && <Icon icon={icon} />}
{label}
{children}
{isBusy && (
<Spinner
className='ui--Button-spinner'
variant='cover'
/>
)}
</StyledButton>
);
}
const ICON_PADDING = 0.5;
const StyledButton = styled.button`
background: transparent;
border: none;
color: inherit;
cursor: pointer;
line-height: 1;
margin: 0;
outline: none;
position: relative;
vertical-align: middle;
text-align: center;
&:not(.hasLabel) {
padding: 0.7em;
.ui--Icon {
padding: 0.6rem;
margin: -0.6rem;
}
}
&:not(.isCircular) {
border-radius: 0.25rem;
}
&:focus {
outline:0;
}
&.hasLabel {
padding: 0.7rem 1.1rem 0.7rem ${1.1 - ICON_PADDING}rem;
.ui--Icon {
margin-right: 0.425rem !important;
}
}
&.isBasic {
background: var(--bg-table);
}
&.isCircular {
border-radius: 10rem;
}
&.isDisabled, &.isReadOnly {
background: none;
box-shadow: none;
cursor: not-allowed;
}
&.isBusy {
cursor: wait;
}
&.isFull {
display: block;
width: 100%;
}
&.isIcon {
background: transparent;
}
.ui--Button-overlay {
background: rgba(253, 252, 251, 0.75);
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
visibility: hidden;
}
.ui--Icon {
border-radius: 50%;
box-sizing: content-box;
height: 1rem;
margin: -${ICON_PADDING}rem 0;
padding: ${ICON_PADDING}rem;
width: 1rem;
}
&.isDisabled {
color: #bcbbba;
}
`;
const Button = React.memo(ButtonBase) as unknown as typeof ButtonBase & {
Group: typeof Group
};
Button.Group = Group;

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
import type { IconName } from '@fortawesome/fontawesome-svg-core';
import type { BareProps } from '../types';
export type Button$Callback = () => void | Promise<void>;
@ -29,15 +28,3 @@ export interface ButtonProps {
tooltip?: React.ReactNode;
withoutLink?: boolean;
}
export type DividerProps = BareProps;
export interface GroupProps {
children?: React.ReactNode;
className?: string;
isCentered?: boolean;
}
export type ButtonType = React.ComponentType<ButtonProps> & {
Group: React.ComponentType<GroupProps>;
};

View File

@ -1,12 +1,15 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BaseProps } from './types';
import React from 'react';
import styled from 'styled-components';
function BaseChart ({ children, className = '' }: BaseProps): React.ReactElement<BaseProps> {
interface Props {
children: React.ReactNode;
className?: string;
}
function BaseChart ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<StyledDiv className={`${className} ui--Chart`}>
{children}

View File

@ -1,7 +1,7 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DoughnutProps } from './types';
import type { BN } from '@polkadot/util';
import React from 'react';
import { Doughnut } from 'react-chartjs-2';
@ -10,6 +10,18 @@ import { bnToBn } from '@polkadot/util';
import Base from './Base';
interface DoughnutValue {
colors: string[];
label: string;
value: number | BN;
}
export interface Props {
className?: string;
size?: number;
values: DoughnutValue[];
}
interface Options {
colorNormal: string[];
colorHover: string[];
@ -17,7 +29,7 @@ interface Options {
labels: string[];
}
function ChartDoughnut ({ className = '', size = 100, values }: DoughnutProps): React.ReactElement<DoughnutProps> {
function ChartDoughnut ({ className = '', size = 100, values }: Props): React.ReactElement<Props> {
const options: Options = {
colorHover: [],
colorNormal: [],
@ -33,7 +45,7 @@ function ChartDoughnut ({ className = '', size = 100, values }: DoughnutProps):
});
return (
<Base className={className}>
<Base className={`${className} ui--Chart-Doughnut`}>
<Doughnut
data={{
datasets: [{

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { ChartData, ChartOptions, TooltipItem } from 'chart.js';
import type { HorizBarProps, HorizBarValue } from './types';
import type { BN } from '@polkadot/util';
import React, { useEffect, useState } from 'react';
import { Bar } from 'react-chartjs-2';
@ -11,6 +11,22 @@ import { bnToBn, isNumber } from '@polkadot/util';
import { alphaColor } from './utils';
interface Value {
colors: string[];
label: string;
tooltip?: string;
value: number | BN;
}
export interface Props {
aspectRatio?: number;
className?: string;
max?: number;
showLabels?: boolean;
values: Value[];
withColors?: boolean;
}
interface State {
chartData?: ChartData;
chartOptions?: ChartOptions;
@ -26,7 +42,7 @@ interface Config {
}[];
}
function calculateOptions (aspectRatio: number, values: HorizBarValue[], jsonValues: string, max: number, showLabels: boolean): State {
function calculateOptions (aspectRatio: number, values: Value[], jsonValues: string, max: number, showLabels: boolean): State {
const chartData = values.reduce((data, { colors: [normalColor = '#00f', hoverColor], label, value }): Config => {
const dataset = data.datasets[0];
@ -70,7 +86,7 @@ function calculateOptions (aspectRatio: number, values: HorizBarValue[], jsonVal
};
}
function ChartHorizBar ({ aspectRatio = 8, className = '', max = 100, showLabels = false, values }: HorizBarProps): React.ReactElement<HorizBarProps> | null {
function ChartHorizBar ({ aspectRatio = 8, className = '', max = 100, showLabels = false, values }: Props): React.ReactElement<Props> | null {
const [{ chartData, chartOptions, jsonValues }, setState] = useState<State>({});
useEffect((): void => {
@ -87,7 +103,7 @@ function ChartHorizBar ({ aspectRatio = 8, className = '', max = 100, showLabels
// HACK on width/height to get the aspectRatio to work
return (
<div className={className}>
<div className={`${className} ui--Chart-HorizBar`}>
<Bar
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data={chartData as any}

View File

@ -3,7 +3,6 @@
import type { ChartData, ChartOptions } from 'chart.js';
import type { BN } from '@polkadot/util';
import type { LineProps } from './types';
import React, { useMemo } from 'react';
import * as Chart from 'react-chartjs-2';
@ -13,6 +12,15 @@ import { isBn, objectSpread } from '@polkadot/util';
import ErrorBoundary from '../ErrorBoundary';
import { alphaColor } from './utils';
export interface Props {
colors?: (string | undefined)[];
className?: string;
labels: string[];
legends: string[];
options?: ChartOptions;
values: (number | BN)[][];
}
interface Dataset {
data: number[];
fill: boolean;
@ -113,7 +121,7 @@ function getData (colors: (string | undefined)[] = [], legends: string[], labels
}, { datasets: [] as Dataset[], labels });
}
function LineChart ({ className, colors, labels, legends, options, values }: LineProps): React.ReactElement<LineProps> | null {
function LineChart ({ className = '', colors, labels, legends, options, values }: Props): React.ReactElement<Props> | null {
const chartOptions = useMemo(
() => getOptions(options),
[options]
@ -125,7 +133,7 @@ function LineChart ({ className, colors, labels, legends, options, values }: Lin
);
return (
<div className={className}>
<div className={`${className} ui--Chart-Line`}>
<ErrorBoundary>
<Chart.Line
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment

View File

@ -1,48 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
import ChartJs from 'chart.js';
export interface DoughnutValue {
colors: string[];
label: string;
value: number | BN;
}
export interface HorizBarValue {
colors: string[];
label: string;
tooltip?: string;
value: number | BN;
}
export interface BaseProps {
children: React.ReactNode;
className?: string;
}
export interface DoughnutProps {
className?: string;
size?: number;
values: DoughnutValue[];
}
export interface HorizBarProps {
aspectRatio?: number;
className?: string;
max?: number;
showLabels?: boolean;
values: HorizBarValue[];
withColors?: boolean;
}
export interface LineProps {
colors?: (string | undefined)[];
className?: string;
labels: string[];
legends: string[];
options?: ChartJs.ChartOptions;
values: (number | BN)[][];
}

View File

@ -57,7 +57,7 @@ function Column ({ children, className = '' }: Props): React.ReactElement<Props>
);
}
function Columar ({ children, className = '', is60, is100, isPadded = true, isReverse, size = 'default' }: Props): React.ReactElement<Props> {
function ColumarBase ({ children, className = '', is60, is100, isPadded = true, isReverse, size = 'default' }: Props): React.ReactElement<Props> {
return (
<StyledDiv className={`${className} ui--Columar ${is100 ? 'is100' : (is60 ? 'is60' : 'is50')} ${isPadded ? 'isPadded' : ''} ${isReverse ? 'isReverse' : ''} ${size}Size`}>
{children}
@ -133,6 +133,10 @@ const StyledDiv = styled.div`
}
`;
const Columar = React.memo(ColumarBase) as unknown as typeof ColumarBase & {
Column: typeof Column
};
Columar.Column = Column;
export default Columar;

View File

@ -44,7 +44,7 @@ export type IDropdown<Option extends DropdownItemProps> = React.ComponentType<Pr
Header: React.ComponentType<{ content: React.ReactNode }>;
}
function Dropdown<Option extends DropdownItemProps> ({ allowAdd = false, children, className = '', defaultValue, dropdownClassName, isButton, isDisabled, isError, isFull, isMultiple, label, labelExtra, onAdd, onBlur, onChange, onClose, onSearch, options, placeholder, renderLabel, searchInput, tabIndex, transform, value, withEllipsis, withLabel }: Props<Option>): React.ReactElement<Props<Option>> {
function DropdownBase<Option extends DropdownItemProps> ({ allowAdd = false, children, className = '', defaultValue, dropdownClassName, isButton, isDisabled, isError, isFull, isMultiple, label, labelExtra, onAdd, onBlur, onChange, onClose, onSearch, options, placeholder, renderLabel, searchInput, tabIndex, transform, value, withEllipsis, withLabel }: Props<Option>): React.ReactElement<Props<Option>> {
const lastUpdate = useRef<string>('');
const [stored, setStored] = useState<string | undefined>();
@ -169,6 +169,10 @@ const StyledLabelled = styled(Labelled)`
}
`;
const Dropdown = React.memo(DropdownBase) as unknown as typeof DropdownBase & {
Header: typeof SUIDropdown.Header
};
Dropdown.Header = SUIDropdown.Header;
export default Dropdown;

View File

@ -1,29 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BaseProps } from './types';
import React from 'react';
import styled from 'styled-components';
function Base ({ children, className = '' }: BaseProps): React.ReactElement<BaseProps> {
return (
<StyledDiv className={`${className} ui--Menu`}>
{children}
</StyledDiv>
);
}
const StyledDiv = styled.div`
display: flex;
flex-direction: column;
min-width: 14.286rem;
margin: 1rem 0;
& > *:not(.ui--Menu__Item):not(.ui--Menu__Divider) {
margin-right: 1rem;
margin-left: 1rem;
}
`;
export default React.memo(Base);

View File

@ -1,22 +1,36 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BaseProps, DividerProps, HeaderProps, ItemProps } from './types';
import type { BaseProps, MenuType } from './types';
import React from 'react';
import styled from 'styled-components';
import MenuBase from './Base';
import Divider from './Divider';
import Header from './Header';
import Item from './Item';
type MenuDef = React.FC<BaseProps> & {
Divider: React.FC<DividerProps>;
Item: React.FC<ItemProps>;
Header: React.FC<HeaderProps>;
};
function Base ({ children, className = '' }: BaseProps): React.ReactElement<BaseProps> {
return (
<StyledDiv className={`${className} ui--Menu`}>
{children}
</StyledDiv>
);
}
const Menu: MenuDef = MenuBase as unknown as MenuDef;
const StyledDiv = styled.div`
display: flex;
flex-direction: column;
min-width: 14.286rem;
margin: 1rem 0;
& > *:not(.ui--Menu__Item):not(.ui--Menu__Divider) {
margin-right: 1rem;
margin-left: 1rem;
}
`;
const Menu = React.memo(Base) as unknown as MenuType;
Menu.Divider = Divider;
Menu.Item = Item;

View File

@ -26,3 +26,9 @@ export interface BaseProps {
export interface DividerProps {
className?: string;
}
export type MenuType = React.FC<BaseProps> & {
Divider: React.FC<DividerProps>;
Item: React.FC<ItemProps>;
Header: React.FC<HeaderProps>;
};

View File

@ -1,16 +1,19 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ActionsProps } from './types';
import React from 'react';
import styled from 'styled-components';
import Button from '../Button';
function Actions ({ children, className = '' }: ActionsProps) {
interface Props {
className?: string;
children: React.ReactNode;
}
function Actions ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<StyledDiv className={className}>
<StyledDiv className={`${className} ui--Modal-Actions`}>
<Button.Group>
{children}
</Button.Group>

View File

@ -1,109 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ModalProps } from './types';
import React, { useCallback, useEffect } from 'react';
import { createPortal } from 'react-dom';
import styled, { createGlobalStyle } from 'styled-components';
import { useTheme } from '@polkadot/react-hooks';
import Header from './Header';
function Base (props: ModalProps): React.ReactElement<ModalProps> {
const { themeClassName } = useTheme();
const { children, className = '', header, onClose, size = 'medium', testId = 'modal' } = props;
const listenKeyboard = useCallback((event: KeyboardEvent) => {
// eslint-disable-next-line deprecation/deprecation
if (event.key === 'Escape' || event.keyCode === 27) {
onClose();
}
}, [onClose]);
useEffect(() => {
window.addEventListener('keydown', listenKeyboard, true);
return () => {
window.removeEventListener('keydown', listenKeyboard, true);
};
}, [listenKeyboard]);
return createPortal(
<StyledDiv
className={`${className} ui--Modal ${size}Size ${themeClassName} `}
data-testid={testId}
>
<DisableGlobalScroll />
<div
className='ui--Modal__overlay'
onClick={onClose}
/>
<div className='ui--Modal__body'>
<Header
header={header}
onClose={onClose}
/>
{children}
</div>
</StyledDiv>,
document.body
);
}
const DisableGlobalScroll = createGlobalStyle`
body {
overflow: hidden;
}
`;
const StyledDiv = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
min-height: 100vh;
z-index: 1000;
overflow-y: auto;
.ui--Modal__overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(96, 96, 96, 0.5);
}
.ui--Modal__body {
margin-top: 30px;
background: var(--bg-page);
border-radius: 4px;
box-shadow: none;
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, 0);
max-width: 900px;
width: calc(100% - 16px);
color: var(--color-text);
font: var(--font-sans);
}
&.smallSize .ui--Modal__body {
max-width: 720px;
}
&.largeSize .ui--Modal__body {
max-width: 1080px;
}
`;
export default React.memo(Base);

View File

@ -1,12 +1,16 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ColumnsProps } from './types';
import React from 'react';
import styled from 'styled-components';
function Columns ({ children, className = '', hint }: ColumnsProps): React.ReactElement<ColumnsProps> {
interface Props {
children: React.ReactNode;
className?: string;
hint?: React.ReactNode;
}
function Columns ({ children, className = '', hint }: Props): React.ReactElement<Props> {
return (
<StyledDiv className={`${className} ui--Modal-Columns`}>
<div>{children}</div>

View File

@ -1,14 +1,17 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ContentProps } from './types';
import React from 'react';
import styled from 'styled-components';
function Content ({ children, className = '' }: ContentProps) {
interface Props {
className?: string;
children: React.ReactNode;
}
function Content ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<StyledDiv className={`${className} ui--Modal__Content`}>
<StyledDiv className={`${className} ui--Modal-Content`}>
{children}
</StyledDiv>
);

View File

@ -1,16 +1,20 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HeaderProps } from './types';
import React from 'react';
import styled from 'styled-components';
import Button from '../Button';
function Header ({ className = '', header, onClose }: HeaderProps) {
interface Props {
className?: string;
header?: React.ReactNode;
onClose: () => void;
}
function Header ({ className = '', header, onClose }: Props): React.ReactElement<Props> {
return (
<StyledDiv className={`${className} ui--Modal__Header`}>
<StyledDiv className={`${className} ui--Modal-Header`}>
{header && (
<h1>{header}</h1>
)}

View File

@ -1,14 +1,126 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ModalType } from './types';
import React, { useCallback, useEffect } from 'react';
import { createPortal } from 'react-dom';
import styled, { createGlobalStyle } from 'styled-components';
import { useTheme } from '@polkadot/react-hooks';
import Actions from './Actions';
import ModalBase from './Base';
import Columns from './Columns';
import Content from './Content';
import Header from './Header';
const Modal = ModalBase as unknown as ModalType;
interface Props {
size?: 'large' | 'medium' | 'small';
children: React.ReactNode;
className?: string;
header?: React.ReactNode;
open?: boolean;
onClose: () => void;
testId?: string;
}
function ModalBase ({ children, className = '', header, onClose, size = 'medium', testId = 'modal' }: Props): React.ReactElement<Props> {
const { themeClassName } = useTheme();
const listenKeyboard = useCallback((event: KeyboardEvent) => {
// eslint-disable-next-line deprecation/deprecation
if (event.key === 'Escape' || event.keyCode === 27) {
onClose();
}
}, [onClose]);
useEffect(() => {
window.addEventListener('keydown', listenKeyboard, true);
return () => {
window.removeEventListener('keydown', listenKeyboard, true);
};
}, [listenKeyboard]);
return createPortal(
<StyledDiv
className={`${className} ui--Modal ${size}Size ${themeClassName} `}
data-testid={testId}
>
<DisableGlobalScroll />
<div
className='ui--Modal__overlay'
onClick={onClose}
/>
<div className='ui--Modal__body'>
<Header
header={header}
onClose={onClose}
/>
{children}
</div>
</StyledDiv>,
document.body
);
}
const DisableGlobalScroll = createGlobalStyle`
body {
overflow: hidden;
}
`;
const StyledDiv = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
min-height: 100vh;
z-index: 1000;
overflow-y: auto;
.ui--Modal__overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(96, 96, 96, 0.5);
}
.ui--Modal__body {
margin-top: 30px;
background: var(--bg-page);
border-radius: 4px;
box-shadow: none;
display: flex;
flex-direction: column;
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, 0);
max-width: 900px;
width: calc(100% - 16px);
color: var(--color-text);
font: var(--font-sans);
}
&.smallSize .ui--Modal__body {
max-width: 720px;
}
&.largeSize .ui--Modal__body {
max-width: 1080px;
}
`;
const Modal = React.memo(ModalBase) as unknown as typeof ModalBase & {
Actions: typeof Actions;
Columns: typeof Columns;
Content: typeof Content;
};
Modal.Actions = Actions;
Modal.Columns = Columns;

View File

@ -1,49 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
export interface HeaderProps {
className?: string;
header?: React.ReactNode;
onClose: () => void;
}
export interface ColumnsProps {
children: React.ReactNode;
className?: string;
hint?: React.ReactNode;
}
type ModalSize = 'large' | 'medium' | 'small'
export interface ModalProps {
size?: ModalSize;
children: React.ReactNode;
className?: string;
header?: React.ReactNode;
open?: boolean;
onClose: () => void;
testId?: string;
[index: string]: any;
}
export interface ActionsProps {
className?: string;
children: React.ReactNode;
}
export interface BodyProps {
size: ModalSize;
}
export interface ContentProps {
className?: string;
children: React.ReactNode;
}
export type ModalType = React.FC<ModalProps> & {
Actions: React.FC<ActionsProps>;
Columns: React.FC<ColumnsProps>;
Content: React.FC<ContentProps>;
};

View File

@ -16,14 +16,16 @@ interface Props {
}
function Body ({ children, className = '', empty, emptySpinner, noBodyTag }: Props): React.ReactElement<Props> {
const bodyClassName = `${className} ui--Table-Body`;
return children
? (
noBodyTag
? <>{children}</>
: <tbody className={className}>{children}</tbody>
: <tbody className={bodyClassName}>{children}</tbody>
)
: (
<tbody className={className}>
<tbody className={bodyClassName}>
<tr>
<td colSpan={100}>{
isString(empty)

View File

@ -1,15 +1,27 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ColBalanceProps as Props } from '../types';
import type { BN } from '@polkadot/util';
import React from 'react';
import { FormatBalance } from '@polkadot/react-query';
function Balance ({ className = '', value, withLoading }: Props): React.ReactElement<Props> {
export interface Props {
className?: string;
colSpan?: number;
rowSpan?: number;
value?: BN | null;
withLoading?: boolean;
}
function Balance ({ className = '', colSpan, rowSpan, value, withLoading }: Props): React.ReactElement<Props> {
return (
<td className={`${className} ui--Table-Column-Balance number`}>
<td
className={`${className} ui--Table-Column-Balance number`}
colSpan={colSpan}
rowSpan={rowSpan}
>
{value
? <FormatBalance value={value} />
: withLoading && (

View File

@ -1,18 +1,26 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ColExpandProps as Props } from '../types';
import React from 'react';
import styled from 'styled-components';
import Icon from '../../Icon';
function Expand ({ className = '', isExpanded, toggle }: Props): React.ReactElement<Props> {
export interface Props {
className?: string;
colSpan?: number;
rowSpan?: number;
isExpanded: boolean;
toggle: () => void;
}
function Expand ({ className = '', colSpan, isExpanded, rowSpan, toggle }: Props): React.ReactElement<Props> {
return (
<StyledTd
className={`${className} ui--Table-Column-Expand`}
colSpan={colSpan}
onClick={toggle}
rowSpan={rowSpan}
>
<div>
<Icon

View File

@ -1,14 +1,21 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ColFavoriteProps as Props } from '../types';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import Icon from '../../Icon';
function Favorite ({ address, className = '', isFavorite, toggle }: Props): React.ReactElement<Props> {
export interface Props {
address: string;
className?: string;
colSpan?: number;
isFavorite: boolean;
rowSpan?: number;
toggle: (address: string) => void;
}
function Favorite ({ address, className = '', colSpan, isFavorite, rowSpan, toggle }: Props): React.ReactElement<Props> {
const onClick = useCallback(
() => toggle(address),
[address, toggle]
@ -17,7 +24,9 @@ function Favorite ({ address, className = '', isFavorite, toggle }: Props): Reac
return (
<StyledTd
className={`${className} ui--Table-Column-Favorite`}
colSpan={colSpan}
onClick={onClick}
rowSpan={rowSpan}
>
<Icon
color={

View File

@ -1,16 +1,27 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ColIdProps as Props } from '../types';
import type { BN } from '@polkadot/util';
import React from 'react';
import styled from 'styled-components';
import { formatNumber } from '@polkadot/util';
function Id ({ className = '', value }: Props): React.ReactElement<Props> {
export interface Props {
className?: string;
colSpan?: number;
rowSpan?: number;
value: BN | number;
}
function Id ({ className = '', colSpan, rowSpan, value }: Props): React.ReactElement<Props> {
return (
<StyledTd className={`${className} ui--Table-Column-Id`}>
<StyledTd
className={`${className} ui--Table-Column-Id`}
colSpan={colSpan}
rowSpan={rowSpan}
>
<h2 className='--digits'>{formatNumber(value)}</h2>
</StyledTd>
);

View File

@ -1,11 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { TableType } from '../types';
import Balance from './Balance';
import Expand from './Expand';
import Favorite from './Favorite';
import Id from './Id';
export const Column: TableType['Column'] = { Balance, Expand, Favorite, Id };

View File

@ -0,0 +1,36 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import Balance from './Balance';
import Expand from './Expand';
import Favorite from './Favorite';
import Id from './Id';
interface Props {
children?: React.ReactNode;
className?: string;
}
function ColumnBase ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<td className={`${className} ui--Table-Column`}>
{children}
</td>
);
}
const Column = React.memo(ColumnBase) as unknown as typeof ColumnBase & {
Balance: typeof Balance,
Expand: typeof Expand,
Favorite: typeof Favorite,
Id: typeof Id
};
Column.Balance = Balance;
Column.Expand = Expand;
Column.Favorite = Favorite;
Column.Id = Id;
export default Column;

View File

@ -16,7 +16,7 @@ function Foot ({ className = '', footer, isEmpty }: Props): React.ReactElement<P
}
return (
<StyledTfoot className={className}>
<StyledTfoot className={`${className} ui--Table-Foot`}>
{footer}
</StyledTfoot>
);

View File

@ -20,7 +20,7 @@ function Head ({ children, className = '', filter, header, isEmpty }: Props): Re
}
return (
<StyledThead className={className}>
<StyledThead className={`${className} ui--Table-Head`}>
{filter && (
<tr className='filter'>
<th colSpan={100}>{filter}</th>

View File

@ -0,0 +1,21 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
interface Props {
children?: React.ReactNode;
className?: string;
}
function RowBase ({ children, className = '' }: Props): React.ReactElement<Props> {
return (
<tr className={`${className} ui--Table-Row`}>
{children}
</tr>
);
}
const Row = React.memo(RowBase) as unknown as typeof RowBase;
export default Row;

View File

@ -1,13 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { TableType } from './types';
import { Column } from './Column';
import TableBase from './Table';
const Table = TableBase as unknown as TableType;
Table.Column = Column;
export default Table;

View File

@ -1,15 +1,31 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { TableProps as Props } from './types';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import Columar from '../Columar';
import Body from './Body';
import Column from './Column';
import Foot from './Foot';
import Head from './Head';
import Row from './Row';
interface Props {
children?: React.ReactNode;
className?: string;
empty?: React.ReactNode | false;
emptySpinner?: React.ReactNode;
filter?: React.ReactNode;
footer?: React.ReactNode;
header?: ([React.ReactNode?, string?, number?, (() => void)?] | false | null | undefined)[];
headerChildren?: React.ReactNode;
isFixed?: boolean;
isInline?: boolean;
isSplit?: boolean;
legend?: React.ReactNode;
noBodyTag?: boolean;
}
function extractBodyChildren (children: React.ReactNode): [boolean, React.ReactNode | React.ReactNode[]] {
if (!Array.isArray(children)) {
@ -22,7 +38,7 @@ function extractBodyChildren (children: React.ReactNode): [boolean, React.ReactN
return [isEmpty, isEmpty ? null : kids];
}
function Table ({ children, className = '', empty, emptySpinner, filter, footer, header, headerChildren, isFixed, isInline, isSplit, legend, noBodyTag }: Props): React.ReactElement<Props> {
function TableBase ({ children, className = '', empty, emptySpinner, filter, footer, header, headerChildren, isFixed, isInline, isSplit, legend, noBodyTag }: Props): React.ReactElement<Props> {
const [isEmpty, bodyChildren] = extractBodyChildren(children);
const splitBody = useMemo(
@ -525,4 +541,12 @@ const StyledDiv = styled.div`
}
`;
export default React.memo(Table);
const Table = React.memo(TableBase) as unknown as typeof TableBase & {
Column: typeof Column,
Row: typeof Row
};
Table.Column = Column;
Table.Row = Row;
export default Table;

View File

@ -1,53 +0,0 @@
// Copyright 2017-2023 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
export interface ColBalanceProps {
className?: string;
value?: BN | null;
withLoading?: boolean;
}
export interface ColExpandProps {
className?: string;
isExpanded: boolean;
toggle: () => void;
}
export interface ColFavoriteProps {
address: string;
className?: string;
isFavorite: boolean;
toggle: (address: string) => void;
}
export interface ColIdProps {
className?: string;
value: BN | number;
}
export interface TableProps {
children?: React.ReactNode;
className?: string;
empty?: React.ReactNode | false;
emptySpinner?: React.ReactNode;
filter?: React.ReactNode;
footer?: React.ReactNode;
header?: ([React.ReactNode?, string?, number?, (() => void)?] | false | null | undefined)[];
headerChildren?: React.ReactNode;
isFixed?: boolean;
isInline?: boolean;
isSplit?: boolean;
legend?: React.ReactNode;
noBodyTag?: boolean;
}
export type TableType = React.ComponentType<TableProps> & {
Column: {
Balance: React.ComponentType<ColBalanceProps>;
Expand: React.ComponentType<ColExpandProps>;
Favorite: React.ComponentType<ColFavoriteProps>;
Id: React.ComponentType<ColIdProps>;
}
};