Merge remote-tracking branch 'soapbox/develop' into cleanup
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -1,15 +1,16 @@
|
||||
import { Placement } from '@popperjs/core';
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
|
||||
import { Emoji, HStack } from 'soapbox/components/ui';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
interface IEmojiButton {
|
||||
/** Unicode emoji character. */
|
||||
emoji: string,
|
||||
/** Event handler when the emoji is clicked. */
|
||||
onClick: React.EventHandler<React.MouseEvent>,
|
||||
/** Keyboard event handler. */
|
||||
onKeyDown?: React.EventHandler<React.KeyboardEvent>,
|
||||
onClick(emoji: string): void
|
||||
/** Extra class name on the <button> element. */
|
||||
className?: string,
|
||||
/** Tab order of the button. */
|
||||
@ -17,104 +18,104 @@ interface IEmojiButton {
|
||||
}
|
||||
|
||||
/** Clickable emoji button that scales when hovered. */
|
||||
const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, onKeyDown, tabIndex }): JSX.Element => {
|
||||
const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabIndex }): JSX.Element => {
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClick(emoji);
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={clsx(className)} onClick={onClick} tabIndex={tabIndex}>
|
||||
<Emoji className='h-8 w-8 duration-100' emoji={emoji} />
|
||||
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
|
||||
<Emoji className='h-6 w-6 duration-100 hover:scale-110' emoji={emoji} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IEmojiSelector {
|
||||
/** List of Unicode emoji characters. */
|
||||
emojis: Iterable<string>,
|
||||
onClose?(): void
|
||||
/** Event handler when an emoji is clicked. */
|
||||
onReact: (emoji: string) => void,
|
||||
/** Event handler when selector is escaped. */
|
||||
onUnfocus: React.KeyboardEventHandler<HTMLDivElement>,
|
||||
onReact(emoji: string): void
|
||||
/** Element that triggers the EmojiSelector Popper */
|
||||
referenceElement: HTMLElement | null
|
||||
placement?: Placement
|
||||
/** Whether the selector should be visible. */
|
||||
visible?: boolean,
|
||||
/** Whether the selector should be focused. */
|
||||
focused?: boolean,
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
/** Panel with a row of emoji buttons. */
|
||||
const EmojiSelector: React.FC<IEmojiSelector> = ({ emojis, onReact, onUnfocus, visible = false, focused = false }): JSX.Element => {
|
||||
const emojiList = Array.from(emojis);
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||
referenceElement,
|
||||
onClose,
|
||||
onReact,
|
||||
placement = 'top',
|
||||
visible = false,
|
||||
}): JSX.Element => {
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const handleReact = (emoji: string): React.EventHandler<React.MouseEvent> => {
|
||||
return (e) => {
|
||||
onReact(emoji);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// `useRef` won't trigger a re-render, while `useState` does.
|
||||
// https://popper.js.org/react-popper/v2/
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (referenceElement?.contains(event.target as Node) || popperElement?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const { styles, attributes, update } = usePopper(referenceElement, popperElement, {
|
||||
placement,
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [-10, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
};
|
||||
}, [referenceElement]);
|
||||
|
||||
const selectPreviousEmoji = (i: number): void => {
|
||||
if (!node.current) return;
|
||||
|
||||
if (i !== 0) {
|
||||
const button: HTMLButtonElement | null = node.current.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`);
|
||||
button?.focus();
|
||||
} else {
|
||||
const button: HTMLButtonElement | null = node.current.querySelector('.emoji-react-selector__emoji:last-child');
|
||||
button?.focus();
|
||||
useEffect(() => {
|
||||
if (visible && update) {
|
||||
update();
|
||||
}
|
||||
};
|
||||
|
||||
const selectNextEmoji = (i: number) => {
|
||||
if (!node.current) return;
|
||||
|
||||
if (i !== emojiList.length - 1) {
|
||||
const button: HTMLButtonElement | null = node.current.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`);
|
||||
button?.focus();
|
||||
} else {
|
||||
const button: HTMLButtonElement | null = node.current.querySelector('.emoji-react-selector__emoji:first-child');
|
||||
button?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (i: number): React.KeyboardEventHandler<HTMLDivElement> => e => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
handleReact(emojiList[i])(e as any);
|
||||
break;
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) selectPreviousEmoji(i);
|
||||
else selectNextEmoji(i);
|
||||
break;
|
||||
case 'Left':
|
||||
case 'ArrowLeft':
|
||||
selectPreviousEmoji(i);
|
||||
break;
|
||||
case 'Right':
|
||||
case 'ArrowRight':
|
||||
selectNextEmoji(i);
|
||||
break;
|
||||
case 'Escape':
|
||||
onUnfocus(e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}, [visible, update]);
|
||||
|
||||
return (
|
||||
<HStack
|
||||
className={clsx('emoji-react-selector z-[999] w-max max-w-[100vw] flex-wrap gap-2 rounded-full bg-white p-3 shadow-md dark:bg-gray-900')}
|
||||
ref={node}
|
||||
<div
|
||||
className={clsx('z-50 transition-opacity duration-100', {
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
})}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{emojiList.map((emoji, i) => (
|
||||
<EmojiButton
|
||||
className='emoji-react-selector__emoji hover:scale-125 focus:scale-125'
|
||||
key={i}
|
||||
emoji={emoji}
|
||||
onClick={handleReact(emoji)}
|
||||
onKeyDown={handleKeyDown(i)}
|
||||
tabIndex={(visible || focused) ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
<HStack
|
||||
className={clsx('z-[999] flex w-max max-w-[100vw] flex-wrap space-x-3 rounded-full bg-white px-3 py-2.5 shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700')}
|
||||
>
|
||||
{Array.from(soapboxConfig.allowedEmoji).map((emoji, i) => (
|
||||
<EmojiButton
|
||||
key={i}
|
||||
emoji={emoji}
|
||||
onClick={onReact}
|
||||
tabIndex={visible ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -15,6 +15,8 @@ interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
transparent?: boolean,
|
||||
/** Predefined styles to display for the button. */
|
||||
theme?: 'seamless' | 'outlined',
|
||||
/** Override the data-testid */
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
/** A clickable icon. */
|
||||
@ -31,7 +33,7 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef
|
||||
'opacity-50': filteredProps.disabled,
|
||||
}, className)}
|
||||
{...filteredProps}
|
||||
data-testid='icon-button'
|
||||
data-testid={filteredProps['data-testid'] || 'icon-button'}
|
||||
>
|
||||
<SvgIcon src={src} className={iconClassName} />
|
||||
|
||||
|
||||
@ -1,13 +1,32 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
||||
|
||||
interface IProgressBar {
|
||||
progress: number,
|
||||
/** Number between 0 and 1 to represent the percentage complete. */
|
||||
progress: number
|
||||
/** Height of the progress bar. */
|
||||
size?: 'sm' | 'md'
|
||||
}
|
||||
|
||||
/** A horizontal meter filled to the given percentage. */
|
||||
const ProgressBar: React.FC<IProgressBar> = ({ progress }) => (
|
||||
<div className='h-2.5 w-full overflow-hidden rounded-full bg-gray-300 dark:bg-primary-800'>
|
||||
<div className='h-full bg-secondary-500' style={{ width: `${Math.floor(progress * 100)}%` }} />
|
||||
const ProgressBar: React.FC<IProgressBar> = ({ progress, size = 'md' }) => (
|
||||
<div
|
||||
className={clsx('h-2.5 w-full overflow-hidden rounded-lg bg-gray-300 dark:bg-primary-800', {
|
||||
'h-2.5': size === 'md',
|
||||
'h-[6px]': size === 'sm',
|
||||
})}
|
||||
>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress * 100) }}>
|
||||
{({ width }) => (
|
||||
<div
|
||||
className='h-full bg-secondary-500'
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ const AnimatedTabs: React.FC<IAnimatedInterface> = ({ children, ...rest }) => {
|
||||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className='absolute h-[3px] w-full bg-primary-200 dark:bg-primary-700'
|
||||
className='absolute h-[3px] w-full bg-primary-200 dark:bg-gray-800'
|
||||
style={{ top }}
|
||||
/>
|
||||
<div
|
||||
|
||||
@ -26,6 +26,8 @@ interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElemen
|
||||
hasError?: boolean,
|
||||
/** Whether or not you can resize the teztarea */
|
||||
isResizeable?: boolean,
|
||||
/** Textarea theme. */
|
||||
theme?: 'default' | 'transparent',
|
||||
}
|
||||
|
||||
/** Textarea with custom styles. */
|
||||
@ -37,6 +39,7 @@ const Textarea = React.forwardRef(({
|
||||
autoGrow = false,
|
||||
maxRows = 10,
|
||||
minRows = 1,
|
||||
theme = 'default',
|
||||
...props
|
||||
}: ITextarea, ref: React.ForwardedRef<HTMLTextAreaElement>) => {
|
||||
const [rows, setRows] = useState<number>(autoGrow ? 1 : 4);
|
||||
@ -72,9 +75,10 @@ const Textarea = React.forwardRef(({
|
||||
ref={ref}
|
||||
rows={rows}
|
||||
onChange={handleChange}
|
||||
className={clsx({
|
||||
'bg-white dark:bg-transparent shadow-sm block w-full sm:text-sm rounded-md text-gray-900 dark:text-gray-100 placeholder:text-gray-600 dark:placeholder:text-gray-600 border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
||||
true,
|
||||
className={clsx('block w-full rounded-md text-gray-900 placeholder:text-gray-600 dark:text-gray-100 dark:placeholder:text-gray-600 sm:text-sm', {
|
||||
'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
||||
theme === 'default',
|
||||
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',
|
||||
'font-mono': isCodeEditor,
|
||||
'text-red-600 border-red-600': hasError,
|
||||
'resize-none': !isResizeable,
|
||||
|
||||
Reference in New Issue
Block a user