mirror of
https://github.com/mastodon/mastodon.git
synced 2026-03-21 18:05:23 -05:00
Emoji text input and character counter components (#38052)
This commit is contained in:
parent
43b0113a4a
commit
3fbb7424fa
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
import { CharacterCounter } from './index';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: CharacterCounter,
|
||||||
|
title: 'Components/CharacterCounter',
|
||||||
|
} satisfies Meta<typeof CharacterCounter>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Required: Story = {
|
||||||
|
args: {
|
||||||
|
currentString: 'Hello, world!',
|
||||||
|
maxLength: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExceedingLimit: Story = {
|
||||||
|
args: {
|
||||||
|
currentString: 'Hello, world!',
|
||||||
|
maxLength: 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Recommended: Story = {
|
||||||
|
args: {
|
||||||
|
currentString: 'Hello, world!',
|
||||||
|
maxLength: 10,
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
interface CharacterCounterProps {
|
||||||
|
currentString: string;
|
||||||
|
maxLength: number;
|
||||||
|
recommended?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmenter = new Intl.Segmenter();
|
||||||
|
|
||||||
|
export const CharacterCounter = polymorphicForwardRef<
|
||||||
|
'span',
|
||||||
|
CharacterCounterProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
currentString,
|
||||||
|
maxLength,
|
||||||
|
as: Component = 'span',
|
||||||
|
recommended = false,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const currentLength = useMemo(
|
||||||
|
() => [...segmenter.segment(currentString)].length,
|
||||||
|
[currentString],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(
|
||||||
|
classes.counter,
|
||||||
|
currentLength > maxLength && !recommended && classes.counterError,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{recommended ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='character_counter.recommended'
|
||||||
|
defaultMessage='{currentLength}/{maxLength} recommended characters'
|
||||||
|
values={{ currentLength, maxLength }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='character_counter.required'
|
||||||
|
defaultMessage='{currentLength}/{maxLength} characters'
|
||||||
|
values={{ currentLength, maxLength }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
CharacterCounter.displayName = 'CharCounter';
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
.counter {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counterError {
|
||||||
|
color: var(--color-text-error);
|
||||||
|
}
|
||||||
29
app/javascript/mastodon/components/emoji/picker_button.tsx
Normal file
29
app/javascript/mastodon/components/emoji/picker_button.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import EmojiPickerDropdown from '@/mastodon/features/compose/containers/emoji_picker_dropdown_container';
|
||||||
|
|
||||||
|
export const EmojiPickerButton: FC<{
|
||||||
|
onPick: (emoji: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}> = ({ onPick, disabled }) => {
|
||||||
|
const handlePick = useCallback(
|
||||||
|
(emoji: unknown) => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof emoji === 'object' && emoji !== null) {
|
||||||
|
if ('native' in emoji && typeof emoji.native === 'string') {
|
||||||
|
onPick(emoji.native);
|
||||||
|
} else if (
|
||||||
|
'shortcode' in emoji &&
|
||||||
|
typeof emoji.shortcode === 'string'
|
||||||
|
) {
|
||||||
|
onPick(`:${emoji.shortcode}:`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled, onPick],
|
||||||
|
);
|
||||||
|
return <EmojiPickerDropdown onPickEmoji={handlePick} disabled={disabled} />;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
.fieldWrapper div:has(:global(.emoji-picker-dropdown)) {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> input,
|
||||||
|
> textarea {
|
||||||
|
padding-inline-end: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> textarea {
|
||||||
|
min-height: 40px; // Button size with 8px margin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldWrapper :global(.emoji-picker-dropdown) {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: 24px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
:global(.icon-button) {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
import type { EmojiInputProps } from './emoji_text_field';
|
||||||
|
import { EmojiTextAreaField, EmojiTextInputField } from './emoji_text_field';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Form Fields/EmojiTextInputField',
|
||||||
|
args: {
|
||||||
|
label: 'Label',
|
||||||
|
hint: 'Hint text',
|
||||||
|
value: 'Insert text with emoji',
|
||||||
|
},
|
||||||
|
render({ value: initialValue = '', ...args }) {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
return <EmojiTextInputField {...args} value={value} onChange={setValue} />;
|
||||||
|
},
|
||||||
|
} satisfies Meta<EmojiInputProps & { disabled?: boolean }>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Simple: Story = {};
|
||||||
|
|
||||||
|
export const WithMaxLength: Story = {
|
||||||
|
args: {
|
||||||
|
maxLength: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithRecommended: Story = {
|
||||||
|
args: {
|
||||||
|
maxLength: 20,
|
||||||
|
recommended: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextArea: Story = {
|
||||||
|
render(args) {
|
||||||
|
const [value, setValue] = useState('Insert text with emoji');
|
||||||
|
return (
|
||||||
|
<EmojiTextAreaField
|
||||||
|
{...args}
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
label='Label'
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
import type {
|
||||||
|
ChangeEvent,
|
||||||
|
ChangeEventHandler,
|
||||||
|
ComponentPropsWithoutRef,
|
||||||
|
Dispatch,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
SetStateAction,
|
||||||
|
} from 'react';
|
||||||
|
import { useCallback, useId, useRef } from 'react';
|
||||||
|
|
||||||
|
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||||
|
import type { OmitUnion } from '@/mastodon/utils/types';
|
||||||
|
|
||||||
|
import { CharacterCounter } from '../character_counter';
|
||||||
|
import { EmojiPickerButton } from '../emoji/picker_button';
|
||||||
|
|
||||||
|
import classes from './emoji_text_field.module.scss';
|
||||||
|
import type { CommonFieldWrapperProps, InputProps } from './form_field_wrapper';
|
||||||
|
import { FormFieldWrapper } from './form_field_wrapper';
|
||||||
|
import { TextArea } from './text_area_field';
|
||||||
|
import type { TextAreaProps } from './text_area_field';
|
||||||
|
import { TextInput } from './text_input_field';
|
||||||
|
|
||||||
|
export type EmojiInputProps = {
|
||||||
|
value?: string;
|
||||||
|
onChange?: Dispatch<SetStateAction<string>>;
|
||||||
|
maxLength?: number;
|
||||||
|
recommended?: boolean;
|
||||||
|
} & Omit<CommonFieldWrapperProps, 'wrapperClassName'>;
|
||||||
|
|
||||||
|
export const EmojiTextInputField: FC<
|
||||||
|
OmitUnion<ComponentPropsWithoutRef<'input'>, EmojiInputProps>
|
||||||
|
> = ({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
hasError,
|
||||||
|
maxLength,
|
||||||
|
recommended,
|
||||||
|
disabled,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const wrapperProps = {
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
hasError,
|
||||||
|
maxLength,
|
||||||
|
recommended,
|
||||||
|
disabled,
|
||||||
|
inputRef,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmojiFieldWrapper {...wrapperProps}>
|
||||||
|
{(inputProps) => (
|
||||||
|
<TextInput
|
||||||
|
{...inputProps}
|
||||||
|
{...otherProps}
|
||||||
|
value={value}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EmojiFieldWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmojiTextAreaField: FC<
|
||||||
|
OmitUnion<Omit<TextAreaProps, 'style'>, EmojiInputProps>
|
||||||
|
> = ({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
maxLength,
|
||||||
|
recommended = false,
|
||||||
|
disabled,
|
||||||
|
hint,
|
||||||
|
hasError,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const wrapperProps = {
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
hasError,
|
||||||
|
maxLength,
|
||||||
|
recommended,
|
||||||
|
disabled,
|
||||||
|
inputRef: textareaRef,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmojiFieldWrapper {...wrapperProps}>
|
||||||
|
{(inputProps) => (
|
||||||
|
<TextArea
|
||||||
|
{...otherProps}
|
||||||
|
{...inputProps}
|
||||||
|
value={value}
|
||||||
|
ref={textareaRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EmojiFieldWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmojiFieldWrapper: FC<
|
||||||
|
EmojiInputProps & {
|
||||||
|
disabled?: boolean;
|
||||||
|
children: (
|
||||||
|
inputProps: InputProps & { onChange: ChangeEventHandler },
|
||||||
|
) => ReactNode;
|
||||||
|
inputRef: RefObject<HTMLTextAreaElement | HTMLInputElement>;
|
||||||
|
}
|
||||||
|
> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
inputRef,
|
||||||
|
maxLength,
|
||||||
|
recommended = false,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
const counterId = useId();
|
||||||
|
|
||||||
|
const handlePickEmoji = useCallback(
|
||||||
|
(emoji: string) => {
|
||||||
|
onChange?.((prev) => {
|
||||||
|
const position = inputRef.current?.selectionStart ?? prev.length;
|
||||||
|
return insertEmojiAtPosition(prev, emoji, position);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, inputRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange?.(event.target.value);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormFieldWrapper
|
||||||
|
className={classes.fieldWrapper}
|
||||||
|
describedById={counterId}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{(inputProps) => (
|
||||||
|
<>
|
||||||
|
{children({ ...inputProps, onChange: handleChange })}
|
||||||
|
<EmojiPickerButton onPick={handlePickEmoji} disabled={disabled} />
|
||||||
|
{maxLength && (
|
||||||
|
<CharacterCounter
|
||||||
|
currentString={value ?? ''}
|
||||||
|
maxLength={maxLength}
|
||||||
|
recommended={recommended}
|
||||||
|
id={counterId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormFieldWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -5,10 +5,12 @@ import { useContext, useId } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { FieldsetNameContext } from './fieldset';
|
import { FieldsetNameContext } from './fieldset';
|
||||||
import classes from './form_field_wrapper.module.scss';
|
import classes from './form_field_wrapper.module.scss';
|
||||||
|
|
||||||
interface InputProps {
|
export interface InputProps {
|
||||||
id: string;
|
id: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
'aria-describedby'?: string;
|
'aria-describedby'?: string;
|
||||||
|
|
@ -20,8 +22,10 @@ interface FieldWrapperProps {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
inputId?: string;
|
inputId?: string;
|
||||||
|
describedById?: string;
|
||||||
inputPlacement?: 'inline-start' | 'inline-end';
|
inputPlacement?: 'inline-start' | 'inline-end';
|
||||||
children: (inputProps: InputProps) => ReactNode;
|
children: (inputProps: InputProps) => ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,7 +34,7 @@ interface FieldWrapperProps {
|
||||||
export type CommonFieldWrapperProps = Pick<
|
export type CommonFieldWrapperProps = Pick<
|
||||||
FieldWrapperProps,
|
FieldWrapperProps,
|
||||||
'label' | 'hint' | 'hasError'
|
'label' | 'hint' | 'hasError'
|
||||||
>;
|
> & { wrapperClassName?: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple form field wrapper for adding a label and hint to enclosed components.
|
* A simple form field wrapper for adding a label and hint to enclosed components.
|
||||||
|
|
@ -42,10 +46,12 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
|
||||||
inputId: inputIdProp,
|
inputId: inputIdProp,
|
||||||
label,
|
label,
|
||||||
hint,
|
hint,
|
||||||
|
describedById,
|
||||||
required,
|
required,
|
||||||
hasError,
|
hasError,
|
||||||
inputPlacement,
|
inputPlacement,
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const uniqueId = useId();
|
const uniqueId = useId();
|
||||||
const inputId = inputIdProp || `${uniqueId}-input`;
|
const inputId = inputIdProp || `${uniqueId}-input`;
|
||||||
|
|
@ -59,7 +65,9 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
|
||||||
id: inputId,
|
id: inputId,
|
||||||
};
|
};
|
||||||
if (hasHint) {
|
if (hasHint) {
|
||||||
inputProps['aria-describedby'] = hintId;
|
inputProps['aria-describedby'] = describedById
|
||||||
|
? `${describedById} ${hintId}`
|
||||||
|
: hintId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = (
|
const input = (
|
||||||
|
|
@ -68,7 +76,7 @@ export const FormFieldWrapper: FC<FieldWrapperProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classes.wrapper}
|
className={classNames(classes.wrapper, className)}
|
||||||
data-has-error={hasError}
|
data-has-error={hasError}
|
||||||
data-input-placement={inputPlacement}
|
data-input-placement={inputPlacement}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { FormFieldWrapper } from './form_field_wrapper';
|
||||||
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||||
import classes from './text_input.module.scss';
|
import classes from './text_input.module.scss';
|
||||||
|
|
||||||
type TextAreaProps =
|
export type TextAreaProps =
|
||||||
| ({ autoSize?: false } & ComponentPropsWithoutRef<'textarea'>)
|
| ({ autoSize?: false } & ComponentPropsWithoutRef<'textarea'>)
|
||||||
| ({ autoSize: true } & TextareaAutosizeProps);
|
| ({ autoSize: true } & TextareaAutosizeProps);
|
||||||
|
|
||||||
|
|
@ -24,17 +24,23 @@ type TextAreaProps =
|
||||||
export const TextAreaField = forwardRef<
|
export const TextAreaField = forwardRef<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
TextAreaProps & CommonFieldWrapperProps
|
TextAreaProps & CommonFieldWrapperProps
|
||||||
>(({ id, label, hint, required, hasError, ...otherProps }, ref) => (
|
>(
|
||||||
<FormFieldWrapper
|
(
|
||||||
label={label}
|
{ id, label, hint, required, hasError, wrapperClassName, ...otherProps },
|
||||||
hint={hint}
|
ref,
|
||||||
required={required}
|
) => (
|
||||||
hasError={hasError}
|
<FormFieldWrapper
|
||||||
inputId={id}
|
label={label}
|
||||||
>
|
hint={hint}
|
||||||
{(inputProps) => <TextArea {...otherProps} {...inputProps} ref={ref} />}
|
required={required}
|
||||||
</FormFieldWrapper>
|
hasError={hasError}
|
||||||
));
|
inputId={id}
|
||||||
|
className={wrapperClassName}
|
||||||
|
>
|
||||||
|
{(inputProps) => <TextArea {...otherProps} {...inputProps} ref={ref} />}
|
||||||
|
</FormFieldWrapper>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
TextAreaField.displayName = 'TextAreaField';
|
TextAreaField.displayName = 'TextAreaField';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,17 @@ interface Props extends TextInputProps, CommonFieldWrapperProps {}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const TextInputField = forwardRef<HTMLInputElement, Props>(
|
export const TextInputField = forwardRef<HTMLInputElement, Props>(
|
||||||
({ id, label, hint, hasError, required, ...otherProps }, ref) => (
|
(
|
||||||
|
{ id, label, hint, hasError, required, wrapperClassName, ...otherProps },
|
||||||
|
ref,
|
||||||
|
) => (
|
||||||
<FormFieldWrapper
|
<FormFieldWrapper
|
||||||
label={label}
|
label={label}
|
||||||
hint={hint}
|
hint={hint}
|
||||||
required={required}
|
required={required}
|
||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
inputId={id}
|
inputId={id}
|
||||||
|
className={wrapperClassName}
|
||||||
>
|
>
|
||||||
{(inputProps) => <TextInput {...otherProps} {...inputProps} ref={ref} />}
|
{(inputProps) => <TextInput {...otherProps} {...inputProps} ref={ref} />}
|
||||||
</FormFieldWrapper>
|
</FormFieldWrapper>
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
onPickEmoji: PropTypes.func.isRequired,
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
onSkinTone: PropTypes.func.isRequired,
|
onSkinTone: PropTypes.func.isRequired,
|
||||||
skinTone: PropTypes.number.isRequired,
|
skinTone: PropTypes.number.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
|
@ -384,7 +385,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, disabled } = this.props;
|
||||||
const title = intl.formatMessage(messages.emoji);
|
const title = intl.formatMessage(messages.emoji);
|
||||||
const { active, loading, placement } = this.state;
|
const { active, loading, placement } = this.state;
|
||||||
|
|
||||||
|
|
@ -396,6 +397,8 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
active={active}
|
active={active}
|
||||||
iconComponent={MoodIcon}
|
iconComponent={MoodIcon}
|
||||||
onClick={this.onToggle}
|
onClick={this.onToggle}
|
||||||
|
disabled={disabled}
|
||||||
|
id="emoji"
|
||||||
inverted
|
inverted
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,8 @@
|
||||||
"callout.dismiss": "Dismiss",
|
"callout.dismiss": "Dismiss",
|
||||||
"carousel.current": "<sr>Slide</sr> {current, number} / {max, number}",
|
"carousel.current": "<sr>Slide</sr> {current, number} / {max, number}",
|
||||||
"carousel.slide": "Slide {current, number} of {max, number}",
|
"carousel.slide": "Slide {current, number} of {max, number}",
|
||||||
|
"character_counter.recommended": "{currentLength}/{maxLength} recommended characters",
|
||||||
|
"character_counter.required": "{currentLength}/{maxLength} characters",
|
||||||
"closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.",
|
"closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.",
|
||||||
"closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
|
"closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
|
||||||
"closed_registrations_modal.find_another_server": "Find another server",
|
"closed_registrations_modal.find_another_server": "Find another server",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import jsxA11Y from 'eslint-plugin-jsx-a11y';
|
||||||
import promisePlugin from 'eslint-plugin-promise';
|
import promisePlugin from 'eslint-plugin-promise';
|
||||||
import react from 'eslint-plugin-react';
|
import react from 'eslint-plugin-react';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
// @ts-expect-error -- No types available for this package
|
||||||
import storybook from 'eslint-plugin-storybook';
|
import storybook from 'eslint-plugin-storybook';
|
||||||
import { globalIgnores } from 'eslint/config';
|
import { globalIgnores } from 'eslint/config';
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
|
|
@ -368,6 +369,7 @@ export default tseslint.config([
|
||||||
files: ['**/*.stories.ts', '**/*.stories.tsx', '.storybook/*'],
|
files: ['**/*.stories.ts', '**/*.stories.tsx', '.storybook/*'],
|
||||||
rules: {
|
rules: {
|
||||||
'import/no-default-export': 'off',
|
'import/no-default-export': 'off',
|
||||||
|
'react-hooks/rules-of-hooks': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user