Merge branch 'next-ui-comments' into 'next'

Add JSDoc comments to all ui components

See merge request soapbox-pub/soapbox-fe!1282
next
Alex Gleason 2 years ago
commit 9883bcab2f

@ -6,11 +6,15 @@ import StillImage from 'soapbox/components/still_image';
const AVATAR_SIZE = 42;
interface IAvatar {
/** URL to the avatar image. */
src: string,
/** Width and height of the avatar in pixels. */
size?: number,
/** Extra class names for the div surrounding the avatar image. */
className?: string,
}
/** Round profile avatar for accounts. */
const Avatar = (props: IAvatar) => {
const { src, size = AVATAR_SIZE, className } = props;

@ -8,20 +8,33 @@ import { useButtonStyles } from './useButtonStyles';
import type { ButtonSizes, ButtonThemes } from './useButtonStyles';
interface IButton {
/** Whether this button expands the width of its container. */
block?: boolean,
/** Elements inside the <button> */
children?: React.ReactNode,
/** @deprecated unused */
classNames?: string,
/** Prevent the button from being clicked. */
disabled?: boolean,
/** URL to an SVG icon to render inside the button. */
icon?: string,
/** Action when the button is clicked. */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void,
/** A predefined button size. */
size?: ButtonSizes,
/** @deprecated unused */
style?: React.CSSProperties,
/** Text inside the button. Takes precedence over `children`. */
text?: React.ReactNode,
/** Makes the button into a navlink, if provided. */
to?: string,
/** Styles the button visually with a predefined theme. */
theme?: ButtonThemes,
/** Whether this button should submit a form by default. */
type?: 'button' | 'submit',
}
/** Customizable button element with various themes. */
const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.Element => {
const {
block = false,

@ -17,12 +17,17 @@ const messages = defineMessages({
});
interface ICard {
/** The type of card. */
variant?: 'rounded',
/** Card size preset. */
size?: 'md' | 'lg' | 'xl',
/** Extra classnames for the <div> element. */
className?: string,
/** Elements inside the card. */
children: React.ReactNode,
}
/** An opaque backdrop to hold a collection of related elements. */
const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant, size = 'md', className, ...filteredProps }, ref): JSX.Element => (
<div
ref={ref}
@ -42,6 +47,7 @@ interface ICardHeader {
onBackClick?: (event: React.MouseEvent) => void
}
/** Typically holds a CardTitle. */
const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }): JSX.Element => {
const intl = useIntl();
@ -74,10 +80,12 @@ interface ICardTitle {
title: string | React.ReactNode
}
/** A card's title. */
const CardTitle = ({ title }: ICardTitle): JSX.Element => (
<Text size='xl' weight='bold' tag='h1' data-testid='card-title'>{title}</Text>
);
/** A card's body. */
const CardBody: React.FC = ({ children }): JSX.Element => (
<div data-testid='card-body'>{children}</div>
);

@ -7,13 +7,19 @@ import Helmet from 'soapbox/components/helmet';
import { Card, CardBody, CardHeader, CardTitle } from '../card/card';
interface IColumn {
/** Route the back button goes to. */
backHref?: string,
/** Column title text. */
label?: string,
/** Whether this column should have a transparent background. */
transparent?: boolean,
/** Whether this column should have a title and back button. */
withHeader?: boolean,
/** Extra class name for top <div> element. */
className?: string,
}
/** A backdrop for the main section of the UI. */
const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedRef<HTMLDivElement>): JSX.Element => {
const { backHref, children, label, transparent = false, withHeader = true, className } = props;

@ -3,6 +3,7 @@ import React from 'react';
import { shortNumberFormat } from 'soapbox/utils/numbers';
interface ICounter {
/** Number this counter should display. */
count: number,
}

@ -4,12 +4,17 @@ import React from 'react';
import { Emoji, HStack } from 'soapbox/components/ui';
interface IEmojiButton {
/** Unicode emoji character. */
emoji: string,
/** Event handler when the emoji is clicked. */
onClick: React.EventHandler<React.MouseEvent>,
/** Extra class name on the <button> element. */
className?: string,
/** Tab order of the button. */
tabIndex?: number,
}
/** Clickable emoji button that scales when hovered. */
const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabIndex }): JSX.Element => {
return (
<button className={classNames(className)} onClick={onClick} tabIndex={tabIndex}>
@ -19,12 +24,17 @@ const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabInd
};
interface IEmojiSelector {
/** List of Unicode emoji characters. */
emojis: Iterable<string>,
/** Event handler when an emoji is clicked. */
onReact: (emoji: string) => void,
/** Whether the selector should be visible. */
visible?: boolean,
/** Whether the selector should be focused. */
focused?: boolean,
}
/** Panel with a row of emoji buttons. */
const EmojiSelector: React.FC<IEmojiSelector> = ({ emojis, onReact, visible = false, focused = false }): JSX.Element => {
const handleReact = (emoji: string): React.EventHandler<React.MouseEvent> => {

@ -4,9 +4,11 @@ import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji';
import { joinPublicPath } from 'soapbox/utils/static';
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
/** Unicode emoji character. */
emoji: string,
}
/** A single emoji image. */
const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
const { emoji, alt, ...rest } = props;
const codepoints = toCodePoints(removeVS16s(emoji));

@ -1,5 +1,6 @@
import React from 'react';
/** Container element to house form actions. */
const FormActions: React.FC = ({ children }) => (
<div className='flex justify-end space-x-2'>
{children}

@ -2,11 +2,15 @@ import React, { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
interface IFormGroup {
/** Input label message. */
hintText?: React.ReactNode,
/** Input hint message. */
labelText: React.ReactNode,
/** Input errors. */
errors?: string[]
}
/** Input element with label and hint. */
const FormGroup: React.FC<IFormGroup> = (props) => {
const { children, errors = [], labelText, hintText } = props;
const formFieldId: string = useMemo(() => `field-${uuidv4()}`, []);

@ -1,10 +1,13 @@
import * as React from 'react';
interface IForm {
/** Form submission event handler. */
onSubmit?: (event: React.FormEvent) => void,
/** Class name override for the <form> element. */
className?: string,
}
/** Form element with custom styles. */
const Form: React.FC<IForm> = ({ onSubmit, children, ...filteredProps }) => {
const handleSubmit = React.useCallback((event) => {
event.preventDefault();

@ -24,14 +24,21 @@ const spaces = {
};
interface IHStack {
/** Vertical alignment of children. */
alignItems?: 'top' | 'bottom' | 'center' | 'start',
/** Extra class names on the <div> element. */
className?: string,
/** Horizontal alignment of children. */
justifyContent?: 'between' | 'center',
/** Size of the gap between elements. */
space?: 0.5 | 1 | 1.5 | 2 | 3 | 4 | 6,
/** Whether to let the flexbox grow. */
grow?: boolean,
/** Extra CSS styles for the <div> */
style?: React.CSSProperties
}
/** Horizontal row of child elements. */
const HStack: React.FC<IHStack> = (props) => {
const { space, alignItems, grow, justifyContent, className, ...filteredProps } = props;

@ -5,12 +5,17 @@ import SvgIcon from '../icon/svg-icon';
import Text from '../text/text';
interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Class name for the <svg> icon. */
iconClassName?: string,
/** URL to the svg icon. */
src: string,
/** Text to display next ot the button. */
text?: string,
/** Don't render a background behind the icon. */
transparent?: boolean
}
/** A clickable icon. */
const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => {
const { src, className, iconClassName, text, transparent = false, ...filteredProps } = props;

@ -4,16 +4,21 @@ import Counter from '../counter/counter';
import SvgIcon from './svg-icon';
interface IIcon extends Pick<React.SVGAttributes<SVGAElement>, 'strokeWidth'> {
/** Class name for the <svg> element. */
className?: string,
/** Number to display a counter over the icon. */
count?: number,
/** Tooltip text for the icon. */
alt?: string,
/** URL to the svg icon. */
src: string,
/** Width and height of the icon in pixels. */
size?: number,
}
const Icon = ({ src, alt, count, size, ...filteredProps }: IIcon): JSX.Element => (
/** Renders and SVG icon with optional counter. */
const Icon: React.FC<IIcon> = ({ src, alt, count, size, ...filteredProps }): JSX.Element => (
<div className='relative' data-testid='icon'>
{count ? (
<span className='absolute -top-2 -right-3'>

@ -2,9 +2,13 @@ import React from 'react';
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
interface ISvgIcon {
/** Class name for the <svg> */
className?: string,
/** Tooltip text for the icon. */
alt?: string,
/** URL to the svg file. */
src: string,
/** Width and height of the icon in pixels. */
size?: number,
}

@ -12,17 +12,27 @@ const messages = defineMessages({
});
interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxLength' | 'onChange' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required' | 'disabled'> {
/** Put the cursor into the input on mount. */
autoFocus?: boolean,
/** The initial text in the input. */
defaultValue?: string,
/** Extra class names for the <input> element. */
className?: string,
/** URL to the svg icon. */
icon?: string,
/** Internal input name. */
name?: string,
/** Text to display before a value is entered. */
placeholder?: string,
/** Text in the input. */
value?: string,
/** Change event handler for the input. */
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
/** HTML input type. */
type: 'text' | 'email' | 'tel' | 'password',
}
/** Form input element. */
const Input = React.forwardRef<HTMLInputElement, IInput>(
(props, ref) => {
const intl = useIntl();

@ -2,13 +2,14 @@ import classNames from 'classnames';
import React from 'react';
import StickyBox from 'react-sticky-box';
interface LayoutType extends React.FC {
interface LayoutComponent extends React.FC {
Sidebar: React.FC,
Main: React.FC<React.HTMLAttributes<HTMLDivElement>>,
Aside: React.FC,
}
const Layout: LayoutType = ({ children }) => (
/** Layout container, to hold Sidebar, Main, and Aside. */
const Layout: LayoutComponent = ({ children }) => (
<div className='sm:pt-4 relative'>
<div className='max-w-3xl mx-auto sm:px-6 md:max-w-7xl md:px-8 md:grid md:grid-cols-12 md:gap-8'>
{children}
@ -16,6 +17,7 @@ const Layout: LayoutType = ({ children }) => (
</div>
);
/** Left sidebar container in the UI. */
const Sidebar: React.FC = ({ children }) => (
<div className='hidden lg:block lg:col-span-3'>
<StickyBox offsetTop={80} className='pb-4'>
@ -24,6 +26,7 @@ const Sidebar: React.FC = ({ children }) => (
</div>
);
/** Center column container in the UI. */
const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className }) => (
<main
className={classNames({
@ -34,6 +37,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
</main>
);
/** Right sidebar container in the UI. */
const Aside: React.FC = ({ children }) => (
<aside className='hidden xl:block xl:col-span-3'>
<StickyBox offsetTop={80} className='space-y-6 pb-12' >

@ -13,10 +13,12 @@ import React from 'react';
import './menu.css';
interface IMenuList extends Omit<MenuPopoverProps, 'position'> {
/** Position of the dropdown menu. */
position?: 'left' | 'right'
}
const MenuList = (props: IMenuList) => (
/** Renders children as a dropdown menu. */
const MenuList: React.FC<IMenuList> = (props) => (
<MenuPopover position={props.position === 'left' ? positionDefault : positionRight}>
<MenuItems
onKeyDown={(event) => event.nativeEvent.stopImmediatePropagation()}
@ -26,6 +28,7 @@ const MenuList = (props: IMenuList) => (
</MenuPopover>
);
/** Divides menu items. */
const MenuDivider = () => <hr />;
export { Menu, MenuButton, MenuDivider, MenuItems, MenuItem, MenuList, MenuLink };

@ -11,18 +11,29 @@ const messages = defineMessages({
});
interface IModal {
/** Callback when the modal is cancelled. */
cancelAction?: () => void,
/** Cancel button text. */
cancelText?: string,
/** Callback when the modal is confirmed. */
confirmationAction?: () => void,
/** Whether the confirmation button is disabled. */
confirmationDisabled?: boolean,
/** Confirmation button text. */
confirmationText?: string,
/** Confirmation button theme. */
confirmationTheme?: 'danger',
/** Callback when the modal is closed. */
onClose?: () => void,
/** Callback when the secondary action is chosen. */
secondaryAction?: () => void,
/** Secondary button text. */
secondaryText?: string,
/** Title text for the modal. */
title: string | React.ReactNode,
}
/** Displays a modal dialog box. */
const Modal: React.FC<IModal> = ({
cancelAction,
cancelText,

@ -1,5 +1,6 @@
import * as React from 'react';
/** Multiple-select dropdown. */
const Select = React.forwardRef<HTMLSelectElement>((props, ref) => {
const { children, ...filteredProps } = props;

@ -7,10 +7,13 @@ import Text from '../text/text';
import './spinner.css';
interface ILoadingIndicator {
/** Width and height of the spinner in pixels. */
size?: number,
/** Whether to display "Loading..." beneath the spinner. */
withText?: boolean
}
/** Spinning loading placeholder. */
const LoadingIndicator = ({ size = 30, withText = true }: ILoadingIndicator) => (
<Stack space={2} justifyContent='center' alignItems='center'>
<div className='spinner' style={{ width: size, height: size }}>

@ -23,12 +23,17 @@ const alignItemsOptions = {
};
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
/** Size of the gap between elements. */
space?: SIZES,
/** Horizontal alignment of children. */
alignItems?: 'center',
/** Vertical alignment of children. */
justifyContent?: 'center',
/** Extra class names on the <div> element. */
className?: string,
}
/** Vertical stack of child elements. */
const Stack: React.FC<IStack> = (props) => {
const { space, alignItems, justifyContent, className, ...filteredProps } = props;

@ -17,10 +17,13 @@ const HORIZONTAL_PADDING = 8;
const AnimatedContext = React.createContext(null);
interface IAnimatedInterface {
/** Callback when a tab is chosen. */
onChange(index: number): void,
/** Default tab index. */
defaultIndex: number
}
/** Tabs with a sliding active state. */
const AnimatedTabs: React.FC<IAnimatedInterface> = ({ children, ...rest }) => {
const [activeRect, setActiveRect] = React.useState(null);
const ref = React.useRef();
@ -58,13 +61,19 @@ const AnimatedTabs: React.FC<IAnimatedInterface> = ({ children, ...rest }) => {
};
interface IAnimatedTab {
/** ARIA role. */
role: 'button',
/** Element to represent the tab. */
as: 'a' | 'button',
/** Route to visit when the tab is chosen. */
href?: string,
/** Tab title text. */
title: string,
/** Index value of the tab. */
index: number
}
/** A single animated tab. */
const AnimatedTab: React.FC<IAnimatedTab> = ({ index, ...props }) => {
// get the currently selected index from useTabsContext
const { selectedIndex } = useTabsContext();
@ -91,20 +100,32 @@ const AnimatedTab: React.FC<IAnimatedTab> = ({ index, ...props }) => {
);
};
/** Structure to represent a tab. */
type Item = {
/** Tab text. */
text: React.ReactNode,
/** Tab tooltip text. */
title?: string,
/** URL to visit when the tab is selected. */
href?: string,
/** Route to visit when the tab is selected. */
to?: string,
/** Callback when the tab is selected. */
action?: () => void,
/** Display a counter over the tab. */
count?: number,
/** Unique name for this tab. */
name: string
}
interface ITabs {
/** Array of structured tab items. */
items: Item[],
/** Name of the active tab item. */
activeItem: string,
}
/** Animated tabs component. */
const Tabs = ({ items, activeItem }: ITabs) => {
const defaultIndex = items.findIndex(({ name }) => name === activeItem);

@ -60,19 +60,29 @@ const families = {
};
interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'dangerouslySetInnerHTML'> {
/** How to align the text. */
align?: Alignments,
/** Extra class names for the outer element. */
className?: string,
dateTime?: string,
/** Typeface of the text. */
family?: Families,
/** Font size of the text. */
size?: Sizes,
/** HTML element name of the outer element. */
tag?: Tags,
/** Theme for the text. */
theme?: Themes,
/** Letter-spacing of the text. */
tracking?: TrackingSizes,
/** Transform (eg uppercase) for the text. */
transform?: TransformProperties,
/** Whether to truncate the text if its container is too small. */
truncate?: boolean,
/** Font weight of the text. */
weight?: Weights
}
/** UI-friendly text container with dark mode support. */
const Text: React.FC<IText> = React.forwardRef(
(props: IText, ref: React.LegacyRef<any>) => {
const {

@ -2,15 +2,23 @@ import classNames from 'classnames';
import React from 'react';
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'required'> {
/** Put the cursor into the input on mount. */
autoFocus?: boolean,
/** The initial text in the input. */
defaultValue?: string,
/** Internal input name. */
name?: string,
/** Renders the textarea as a code editor. */
isCodeEditor?: boolean,
/** Text to display before a value is entered. */
placeholder?: string,
/** Text in the textarea. */
value?: string,
/** Whether the device should autocomplete text in this textarea. */
autoComplete?: string,
}
/** Textarea with custom styles. */
const Textarea = React.forwardRef(
({ isCodeEditor = false, ...props }: ITextarea, ref: React.ForwardedRef<HTMLTextAreaElement>) => {
return (

@ -4,9 +4,11 @@ import React from 'react';
import './tooltip.css';
interface ITooltip {
/** Text to display in the tooltip. */
text: string,
}
/** Hoverable tooltip element. */
const Tooltip: React.FC<ITooltip> = ({
children,
text,

@ -5,24 +5,32 @@ import HStack from 'soapbox/components/ui/hstack/hstack';
import Stack from 'soapbox/components/ui/stack/stack';
interface IWidgetTitle {
title: string | React.ReactNode,
/** Title text for the widget. */
title: React.ReactNode,
}
/** Title of a widget. */
const WidgetTitle = ({ title }: IWidgetTitle): JSX.Element => (
<Text size='xl' weight='bold' tag='h1'>{title}</Text>
);
/** Body of a widget. */
const WidgetBody: React.FC = ({ children }): JSX.Element => (
<Stack space={3}>{children}</Stack>
);
interface IWidget {
title: string | React.ReactNode,
/** Widget title text. */
title: React.ReactNode,
/** Callback when the widget action is clicked. */
onActionClick?: () => void,
/** URL to the svg icon for the widget action. */
actionIcon?: string,
/** Text for the action. */
actionTitle?: string,
}
/** Sidebar widget. */
const Widget: React.FC<IWidget> = ({
title,
children,

Loading…
Cancel
Save