Improvements to shared library components

This commit is contained in:
Josh Perez 2021-07-21 16:45:41 -04:00 committed by GitHub
parent 2c59c71872
commit d9e90e9ea8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 485 additions and 348 deletions

View file

@ -22,7 +22,7 @@ export const GroupDescriptionInput = forwardRef<HTMLInputElement, PropsType>(
i18n={i18n}
onChange={onChangeValue}
placeholder={i18n('setGroupMetadata__group-description-placeholder')}
maxGraphemeCount={256}
maxLengthCount={256}
ref={ref}
value={value}
whenToShowRemainingCount={150}

View file

@ -21,7 +21,7 @@ export const GroupTitleInput = forwardRef<HTMLInputElement, PropsType>(
i18n={i18n}
onChange={onChangeValue}
placeholder={i18n('setGroupMetadata__group-name-placeholder')}
maxGraphemeCount={32}
maxLengthCount={32}
ref={ref}
value={value}
/>

View file

@ -21,7 +21,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
hasClearButton: Boolean(overrideProps.hasClearButton),
i18n,
icon: overrideProps.icon,
maxGraphemeCount: overrideProps.maxGraphemeCount,
maxLengthCount: overrideProps.maxLengthCount,
onChange: action('onChange'),
placeholder: text(
'placeholder',
@ -51,7 +51,7 @@ stories.add('hasClearButton', () => (
stories.add('character count', () => (
<Controller
{...createProps({
maxGraphemeCount: 10,
maxLengthCount: 10,
})}
/>
));
@ -59,7 +59,7 @@ stories.add('character count', () => (
stories.add('character count (customizable show)', () => (
<Controller
{...createProps({
maxGraphemeCount: 64,
maxLengthCount: 64,
whenToShowRemainingCount: 32,
})}
/>
@ -78,7 +78,7 @@ stories.add('expandable w/count', () => (
{...createProps({
expandable: true,
hasClearButton: true,
maxGraphemeCount: 140,
maxLengthCount: 140,
whenToShowRemainingCount: 0,
})}
/>

View file

@ -15,12 +15,13 @@ import { getClassNamesFor } from '../util/getClassNamesFor';
import { multiRef } from '../util/multiRef';
export type PropsType = {
countLength?: (value: string) => number;
disabled?: boolean;
expandable?: boolean;
hasClearButton?: boolean;
i18n: LocalizerType;
icon?: ReactNode;
maxGraphemeCount?: number;
maxLengthCount?: number;
moduleClassName?: string;
onChange: (value: string) => unknown;
placeholder: string;
@ -29,7 +30,7 @@ export type PropsType = {
};
/**
* Some inputs must have fewer than maxGraphemeCount glyphs. Ideally, we'd use the
* Some inputs must have fewer than maxLengthCount glyphs. Ideally, we'd use the
* `maxLength` property on inputs, but that doesn't account for glyphs that are more than
* one UTF-16 code units. For example: `'💩💩'.length === 4`.
*
@ -51,12 +52,13 @@ export const Input = forwardRef<
>(
(
{
countLength = grapheme.count,
disabled,
expandable,
hasClearButton,
i18n,
icon,
maxGraphemeCount = 0,
maxLengthCount = 0,
moduleClassName,
onChange,
placeholder,
@ -108,9 +110,9 @@ export const Input = forwardRef<
const newValue = inputEl.value;
const newGraphemeCount = maxGraphemeCount ? grapheme.count(newValue) : 0;
const newLengthCount = maxLengthCount ? countLength(newValue) : 0;
if (newGraphemeCount <= maxGraphemeCount) {
if (newLengthCount <= maxLengthCount) {
onChange(newValue);
} else {
inputEl.value = valueOnKeydownRef.current;
@ -119,12 +121,12 @@ export const Input = forwardRef<
}
maybeSetLarge();
}, [maxGraphemeCount, maybeSetLarge, onChange]);
}, [countLength, maxLengthCount, maybeSetLarge, onChange]);
const handlePaste = useCallback(
(event: ClipboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const inputEl = innerRef.current;
if (!inputEl || !maxGraphemeCount) {
if (!inputEl || !maxLengthCount) {
return;
}
@ -136,25 +138,25 @@ export const Input = forwardRef<
const pastedText = event.clipboardData.getData('Text');
const newGraphemeCount =
grapheme.count(textBeforeSelection) +
grapheme.count(pastedText) +
grapheme.count(textAfterSelection);
const newLengthCount =
countLength(textBeforeSelection) +
countLength(pastedText) +
countLength(textAfterSelection);
if (newGraphemeCount > maxGraphemeCount) {
if (newLengthCount > maxLengthCount) {
event.preventDefault();
}
maybeSetLarge();
},
[maxGraphemeCount, maybeSetLarge, value]
[countLength, maxLengthCount, maybeSetLarge, value]
);
useEffect(() => {
maybeSetLarge();
}, [maybeSetLarge]);
const graphemeCount = maxGraphemeCount ? grapheme.count(value) : -1;
const lengthCount = maxLengthCount ? countLength(value) : -1;
const getClassName = getClassNamesFor('Input', moduleClassName);
const inputProps = {
@ -187,9 +189,9 @@ export const Input = forwardRef<
/>
) : null;
const graphemeCountElement = graphemeCount >= whenToShowRemainingCount && (
const lengthCountElement = lengthCount >= whenToShowRemainingCount && (
<div className={getClassName('__remaining-count')}>
{maxGraphemeCount - graphemeCount}
{maxLengthCount - lengthCount}
</div>
);
@ -208,12 +210,12 @@ export const Input = forwardRef<
{clearButtonElement}
</div>
<div className={getClassName('__remaining-count--large')}>
{graphemeCountElement}
{lengthCountElement}
</div>
</>
) : (
<div className={getClassName('__controls')}>
{graphemeCountElement}
{lengthCountElement}
{clearButtonElement}
</div>
)}

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useRef, useState } from 'react';
import * as grapheme from '../util/grapheme';
import { AvatarInputContainer } from './AvatarInputContainer';
import { AvatarInputType } from './AvatarInput';
@ -141,9 +140,19 @@ export const ProfileEditor = ({
[setAvatarData]
);
const calculateGraphemeCount = useCallback((name = '') => {
return 256 - grapheme.count(name);
}, []);
const getTextEncoder = useCallback(() => new TextEncoder(), []);
const countByteLength = useCallback(
(str: string) => getTextEncoder().encode(str).byteLength,
[getTextEncoder]
);
const calculateLengthCount = useCallback(
(name = '') => {
return 256 - countByteLength(name);
},
[countByteLength]
);
useEffect(() => {
const focusNode = focusInputRef.current;
@ -155,11 +164,18 @@ export const ProfileEditor = ({
}, [editState]);
if (editState === EditState.ProfileName) {
const shouldDisableSave =
!stagedProfile.firstName ||
(stagedProfile.firstName === fullName.firstName &&
stagedProfile.familyName === fullName.familyName);
content = (
<>
<Input
countLength={countByteLength}
i18n={i18n}
maxGraphemeCount={calculateGraphemeCount(stagedProfile.familyName)}
maxLengthCount={calculateLengthCount(stagedProfile.familyName)}
whenToShowRemainingCount={0}
onChange={newFirstName => {
setStagedProfile(profileData => ({
...profileData,
@ -171,8 +187,10 @@ export const ProfileEditor = ({
value={stagedProfile.firstName}
/>
<Input
countLength={countByteLength}
i18n={i18n}
maxGraphemeCount={calculateGraphemeCount(stagedProfile.firstName)}
maxLengthCount={calculateLengthCount(stagedProfile.firstName)}
whenToShowRemainingCount={0}
onChange={newFamilyName => {
setStagedProfile(profileData => ({
...profileData,
@ -208,14 +226,14 @@ export const ProfileEditor = ({
{i18n('cancel')}
</Button>
<Button
disabled={!stagedProfile.firstName}
disabled={shouldDisableSave}
onClick={() => {
if (!stagedProfile.firstName) {
return;
}
setFullName({
firstName,
familyName,
firstName: stagedProfile.firstName,
familyName: stagedProfile.familyName,
});
onProfileChanged(stagedProfile, avatarData);
@ -228,6 +246,10 @@ export const ProfileEditor = ({
</>
);
} else if (editState === EditState.Bio) {
const shouldDisableSave =
stagedProfile.aboutText === fullBio.aboutText &&
stagedProfile.aboutEmoji === fullBio.aboutEmoji;
content = (
<>
<Input
@ -247,7 +269,7 @@ export const ProfileEditor = ({
/>
</div>
}
maxGraphemeCount={140}
maxLengthCount={140}
moduleClassName="ProfileEditor__about-input"
onChange={value => {
if (value) {
@ -317,6 +339,7 @@ export const ProfileEditor = ({
{i18n('cancel')}
</Button>
<Button
disabled={shouldDisableSave}
onClick={() => {
setFullBio({
aboutEmoji: stagedProfile.aboutEmoji,

View file

@ -4,6 +4,7 @@
import React, {
FormEventHandler,
FunctionComponent,
useCallback,
useRef,
useState,
} from 'react';
@ -111,6 +112,13 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
makeRequest(request);
};
const handleAvatarLoaded = useCallback(
loadedAvatar => {
setAvatar(loadedAvatar);
},
[setAvatar]
);
return (
<Modal
hasXButton
@ -131,9 +139,7 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
setAvatar(newAvatar);
setHasAvatarChanged(true);
}}
onAvatarLoaded={loadedAvatar => {
setAvatar(loadedAvatar);
}}
onAvatarLoaded={handleAvatarLoaded}
variant={AvatarInputVariant.Dark}
/>