Merge remote-tracking branch 'soapbox/develop' into mastodon-groups

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2023-01-27 23:04:42 +01:00
443 changed files with 10101 additions and 30568 deletions

View File

@@ -2,9 +2,12 @@ import classNames from 'clsx';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { HStack, Icon, Text } from 'soapbox/components/ui';
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
import HStack from '../hstack/hstack';
import Icon from '../icon/icon';
import Text from '../text/text';
import type { Menu } from 'soapbox/components/dropdown-menu';
const messages = defineMessages({

View File

@@ -49,6 +49,8 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
className,
} = props;
const body = text || children;
const themeClass = useButtonStyles({
theme,
block,
@@ -61,10 +63,10 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
return null;
}
return <Icon src={icon} className='mr-2 w-4 h-4' />;
return <Icon src={icon} className='w-4 h-4' />;
};
const handleClick = React.useCallback((event) => {
const handleClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback((event) => {
if (onClick && !disabled) {
onClick(event);
}
@@ -72,7 +74,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
const renderButton = () => (
<button
className={classNames(themeClass, className)}
className={classNames('space-x-2 rtl:space-x-reverse', themeClass, className)}
disabled={disabled}
onClick={handleClick}
ref={ref}
@@ -80,7 +82,10 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
data-testid='button'
>
{renderIcon()}
{text || children}
{body && (
<span>{body}</span>
)}
</button>
);

View File

@@ -45,6 +45,7 @@ interface ICardHeader {
backHref?: string,
onBackClick?: (event: React.MouseEvent) => void
className?: string
children?: React.ReactNode
}
/**
@@ -64,7 +65,7 @@ const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBa
return (
<Comp {...backAttributes} className='text-gray-900 dark:text-gray-100 focus:ring-primary-500 focus:ring-2' aria-label={intl.formatMessage(messages.back)}>
<SvgIcon src={require('@tabler/icons/arrow-left.svg')} className='h-6 w-6' />
<SvgIcon src={require('@tabler/icons/arrow-left.svg')} className='h-6 w-6 rtl:rotate-180' />
<span className='sr-only' data-testid='back-button'>{intl.formatMessage(messages.back)}</span>
</Comp>
);
@@ -91,6 +92,8 @@ const CardTitle: React.FC<ICardTitle> = ({ title }): JSX.Element => (
interface ICardBody {
/** Classnames for the <div> element. */
className?: string
/** Children to appear inside the card. */
children: React.ReactNode
}
/** A card's body. */

View File

@@ -46,6 +46,8 @@ export interface IColumn {
className?: string,
/** Ref forwarded to column. */
ref?: React.Ref<HTMLDivElement>
/** Children to display in the column. */
children?: React.ReactNode
}
/** A backdrop for the main section of the UI. */

View File

@@ -20,7 +20,7 @@ const Datepicker = ({ onChange }: IDatepicker) => {
const [month, setMonth] = useState<number>(new Date().getMonth());
const [day, setDay] = useState<number>(new Date().getDate());
const [year, setYear] = useState<number>(2022);
const [year, setYear] = useState<number>(new Date().getFullYear());
const numberOfDays = useMemo(() => {
return getDaysInMonth(month, year);

View File

@@ -2,8 +2,12 @@ import React from 'react';
import HStack from '../hstack/hstack';
interface IFormActions {
children: React.ReactNode
}
/** Container element to house form actions. */
const FormActions: React.FC = ({ children }) => (
const FormActions: React.FC<IFormActions> = ({ children }) => (
<HStack space={2} justifyContent='end'>
{children}
</HStack>

View File

@@ -3,8 +3,6 @@ import React from 'react';
import { render, screen } from '../../../../jest/test-helpers';
import FormGroup from '../form-group';
jest.mock('uuid', () => jest.requireActual('uuid'));
describe('<FormGroup />', () => {
it('connects the label and input', () => {
render(

View File

@@ -14,6 +14,8 @@ interface IFormGroup {
hintText?: React.ReactNode,
/** Input errors. */
errors?: string[]
/** Elements to display within the FormGroup. */
children: React.ReactNode
}
/** Input container with label. Renders the child. */
@@ -27,7 +29,7 @@ const FormGroup: React.FC<IFormGroup> = (props) => {
if (React.isValidElement(inputChildren[0])) {
firstChild = React.cloneElement(
inputChildren[0],
{ id: formFieldId, hasError },
{ id: formFieldId },
);
}
const isCheckboxFormGroup = firstChild?.type === Checkbox;

View File

@@ -5,11 +5,13 @@ interface IForm {
onSubmit?: (event: React.FormEvent) => void,
/** Class name override for the <form> element. */
className?: string,
/** Elements to display within the Form. */
children: React.ReactNode,
}
/** Form element with custom styles. */
const Form: React.FC<IForm> = ({ onSubmit, children, ...filteredProps }) => {
const handleSubmit = React.useCallback((event) => {
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
event.preventDefault();
if (onSubmit) {

View File

@@ -49,6 +49,7 @@ export { default as Tabs } from './tabs/tabs';
export { default as TagInput } from './tag-input/tag-input';
export { default as Text } from './text/text';
export { default as Textarea } from './textarea/textarea';
export { default as Toast } from './toast/toast';
export { default as Toggle } from './toggle/toggle';
export { default as Tooltip } from './tooltip/tooltip';
export { default as Widget } from './widget/widget';

View File

@@ -33,8 +33,6 @@ interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxL
value?: string | number,
/** Change event handler for the input. */
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
/** Whether to display the input in red. */
hasError?: boolean,
/** An element to display as prefix to input. Cannot be used with icon. */
prepend?: React.ReactElement,
/** An element to display as suffix to input. Cannot be used with password type. */
@@ -48,7 +46,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
(props, ref) => {
const intl = useIntl();
const { type = 'text', icon, className, outerClassName, hasError, append, prepend, theme = 'normal', ...filteredProps } = props;
const { type = 'text', icon, className, outerClassName, append, prepend, theme = 'normal', ...filteredProps } = props;
const [revealed, setRevealed] = React.useState(false);
@@ -91,7 +89,6 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white': theme === 'search',
'pr-7 rtl:pl-7 rtl:pr-3': isPassword || append,
'text-red-600 border-red-600': hasError,
'pl-8': typeof icon !== 'undefined',
'pl-16': typeof prepend !== 'undefined',
}, className)}

View File

@@ -2,10 +2,21 @@ import classNames from 'clsx';
import React from 'react';
import StickyBox from 'react-sticky-box';
interface LayoutComponent extends React.FC {
Sidebar: React.FC,
interface ISidebar {
children: React.ReactNode
}
interface IAside {
children?: React.ReactNode
}
interface ILayout {
children: React.ReactNode
}
interface LayoutComponent extends React.FC<ILayout> {
Sidebar: React.FC<ISidebar>,
Main: React.FC<React.HTMLAttributes<HTMLDivElement>>,
Aside: React.FC,
Aside: React.FC<IAside>,
}
/** Layout container, to hold Sidebar, Main, and Aside. */
@@ -18,7 +29,7 @@ const Layout: LayoutComponent = ({ children }) => (
);
/** Left sidebar container in the UI. */
const Sidebar: React.FC = ({ children }) => (
const Sidebar: React.FC<ISidebar> = ({ children }) => (
<div className='hidden lg:block lg:col-span-3'>
<StickyBox offsetTop={80} className='pb-4'>
{children}
@@ -38,7 +49,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
);
/** Right sidebar container in the UI. */
const Aside: React.FC = ({ children }) => (
const Aside: React.FC<IAside> = ({ children }) => (
<aside className='hidden xl:block xl:col-span-3'>
<StickyBox offsetTop={80} className='space-y-6 pb-12'>
{children}

View File

@@ -54,6 +54,7 @@ interface IModal {
/** Title text for the modal. */
title?: React.ReactNode,
width?: keyof typeof widths,
children?: React.ReactNode,
}
/** Displays a modal dialog box. */
@@ -104,7 +105,7 @@ const Modal: React.FC<IModal> = ({
src={closeIcon}
title={intl.formatMessage(messages.close)}
onClick={onClose}
className='text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200'
className='text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200 rtl:rotate-180'
/>
)}
</div>

View File

@@ -11,7 +11,7 @@ const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
return (
<select
ref={ref}
className={`w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-800 focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:ring-primary-500 dark:focus:border-primary-500 sm:text-sm rounded-md disabled:opacity-50 ${className}`}
className={`w-full pl-3 pr-10 py-2 text-base truncate border-gray-300 dark:border-gray-800 focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:ring-primary-500 dark:focus:border-primary-500 sm:text-sm rounded-md disabled:opacity-50 ${className}`}
{...filteredProps}
>
{children}

View File

@@ -21,6 +21,7 @@ interface IAnimatedInterface {
onChange(index: number): void,
/** Default tab index. */
defaultIndex: number
children: React.ReactNode
}
/** Tabs with a sliding active state. */

View File

@@ -0,0 +1,145 @@
import classNames from 'clsx';
import React from 'react';
import toast, { Toast as RHToast } from 'react-hot-toast';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { ToastText, ToastType } from 'soapbox/toast';
import HStack from '../hstack/hstack';
import Icon from '../icon/icon';
const renderText = (text: ToastText) => {
if (typeof text === 'string') {
return text;
} else {
return <FormattedMessage {...text} />;
}
};
interface IToast {
t: RHToast
message: ToastText
type: ToastType
action?(): void
actionLink?: string
actionLabel?: ToastText
}
/**
* Customizable Toasts for in-app notifications.
*/
const Toast = (props: IToast) => {
const { t, message, type, action, actionLink, actionLabel } = props;
const dismissToast = () => toast.dismiss(t.id);
const renderIcon = () => {
switch (type) {
case 'success':
return (
<Icon
src={require('@tabler/icons/circle-check.svg')}
className='w-6 h-6 text-success-500 dark:text-success-400'
aria-hidden
/>
);
case 'info':
return (
<Icon
src={require('@tabler/icons/info-circle.svg')}
className='w-6 h-6 text-primary-600 dark:text-accent-blue'
aria-hidden
/>
);
case 'error':
return (
<Icon
src={require('@tabler/icons/alert-circle.svg')}
className='w-6 h-6 text-danger-600'
aria-hidden
/>
);
}
};
const renderAction = () => {
const classNames = 'mt-0.5 flex-shrink-0 rounded-full text-sm font-medium text-primary-600 dark:text-accent-blue hover:underline focus:outline-none';
if (action && actionLabel) {
return (
<button
type='button'
className={classNames}
onClick={() => {
dismissToast();
action();
}}
data-testid='toast-action'
>
{renderText(actionLabel)}
</button>
);
}
if (actionLink && actionLabel) {
return (
<Link
to={actionLink}
onClick={dismissToast}
className={classNames}
data-testid='toast-action-link'
>
{renderText(actionLabel)}
</Link>
);
}
return null;
};
return (
<div
data-testid='toast'
className={
classNames({
'p-4 pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white dark:bg-gray-900 shadow-lg dark:ring-2 dark:ring-gray-800': true,
'animate-enter': t.visible,
'animate-leave': !t.visible,
})
}
>
<HStack space={4} alignItems='start'>
<HStack space={3} justifyContent='between' alignItems='start' className='w-0 flex-1'>
<HStack space={3} alignItems='start' className='w-0 flex-1'>
<div className='flex-shrink-0'>
{renderIcon()}
</div>
<p className='pt-0.5 text-sm text-gray-900 dark:text-gray-100' data-testid='toast-message'>
{renderText(message)}
</p>
</HStack>
{/* Action */}
{renderAction()}
</HStack>
{/* Dismiss Button */}
<div className='flex flex-shrink-0 pt-0.5'>
<button
type='button'
className='inline-flex rounded-md text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
onClick={dismissToast}
data-testid='toast-dismiss'
>
<span className='sr-only'>Close</span>
<Icon src={require('@tabler/icons/x.svg')} className='w-5 h-5' />
</button>
</div>
</HStack>
</div>
);
};
export default Toast;

View File

@@ -7,6 +7,8 @@ import './tooltip.css';
interface ITooltip {
/** Text to display in the tooltip. */
text: string,
/** Element to display the tooltip around. */
children: React.ReactNode,
}
const centered = (triggerRect: any, tooltipRect: any) => {

View File

@@ -12,8 +12,12 @@ const WidgetTitle = ({ title }: IWidgetTitle): JSX.Element => (
<Text size='xl' weight='bold' tag='h1'>{title}</Text>
);
interface IWidgetBody {
children: React.ReactNode
}
/** Body of a widget. */
const WidgetBody: React.FC = ({ children }): JSX.Element => (
const WidgetBody: React.FC<IWidgetBody> = ({ children }): JSX.Element => (
<Stack space={3}>{children}</Stack>
);
@@ -27,6 +31,7 @@ interface IWidget {
/** Text for the action. */
actionTitle?: string,
action?: JSX.Element,
children?: React.ReactNode,
}
/** Sidebar widget. */
@@ -44,7 +49,7 @@ const Widget: React.FC<IWidget> = ({
<WidgetTitle title={title} />
{action || (onActionClick && (
<IconButton
className='w-6 h-6 ml-2 text-black dark:text-white'
className='w-6 h-6 ml-2 text-black dark:text-white rtl:rotate-180'
src={actionIcon}
onClick={onActionClick}
title={actionTitle}