0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00

refactor(console): improve invitation email input field (#5615)

This commit is contained in:
Charles Zhao 2024-04-02 15:26:41 +08:00 committed by GitHub
parent e09318d3e8
commit d1c41a2fa7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 74 deletions

View file

@ -4,8 +4,8 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
min-height: 96px; min-height: 102px;
padding: 0 _.unit(2) 0 _.unit(3); padding: _.unit(1.5) _.unit(3);
background: var(--color-layer-1); background: var(--color-layer-1);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
@ -17,11 +17,12 @@
cursor: pointer; cursor: pointer;
position: relative; position: relative;
&.multiple { .wrapper {
display: flex;
align-items: center;
justify-content: flex-start; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
gap: _.unit(2); gap: _.unit(2);
padding: _.unit(1.5) _.unit(3);
cursor: text; cursor: text;
.tag { .tag {
@ -58,8 +59,7 @@
color: var(--color-text); color: var(--color-text);
font: var(--font-body-2); font: var(--font-body-2);
background: transparent; background: transparent;
flex-grow: 1; flex: 1;
padding: _.unit(0.5);
&::placeholder { &::placeholder {
color: var(--color-placeholder); color: var(--color-placeholder);
@ -81,6 +81,10 @@
} }
} }
canvas {
display: none;
}
.errorMessage { .errorMessage {
font: var(--font-body-2); font: var(--font-body-2);
color: var(--color-error); color: var(--color-error);

View file

@ -2,7 +2,7 @@ import { emailRegEx } from '@logto/core-kit';
import { generateStandardShortId } from '@logto/shared/universal'; import { generateStandardShortId } from '@logto/shared/universal';
import { conditional, type Nullable } from '@silverhand/essentials'; import { conditional, type Nullable } from '@silverhand/essentials';
import classNames from 'classnames'; import classNames from 'classnames';
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import Close from '@/assets/icons/close.svg'; import Close from '@/assets/icons/close.svg';
@ -23,6 +23,13 @@ type Props = {
placeholder?: string; placeholder?: string;
}; };
/**
* The body-2 font declared in @logto/core-kit/scss/fonts. It is referenced here to calculate
* the width of the input text, which determines the minimum width of the input field.
*/
const fontBody2 =
'400 14px / 20px -apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji';
function InviteEmailsInput({ function InviteEmailsInput({
className, className,
values, values,
@ -35,6 +42,18 @@ function InviteEmailsInput({
const [currentValue, setCurrentValue] = useState(''); const [currentValue, setCurrentValue] = useState('');
const { setError, clearErrors } = useFormContext<InviteMemberForm>(); const { setError, clearErrors } = useFormContext<InviteMemberForm>();
const { parseEmailOptions } = useEmailInputUtils(); const { parseEmailOptions } = useEmailInputUtils();
const [minInputWidth, setMinInputWidth] = useState<number>(0);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
// Render placeholder text in canvas to calculate its width in CSS pixels.
const ctx = canvasRef.current?.getContext('2d');
if (!ctx) {
return;
}
ctx.font = fontBody2;
setMinInputWidth(ctx.measureText(currentValue).width);
}, [currentValue]);
const onChange = (values: InviteeEmailItem[]) => { const onChange = (values: InviteeEmailItem[]) => {
const { values: parsedValues, errorMessage } = parseEmailOptions(values); const { values: parsedValues, errorMessage } = parseEmailOptions(values);
@ -67,12 +86,7 @@ function InviteEmailsInput({
return ( return (
<> <>
<div <div
className={classNames( className={classNames(styles.input, Boolean(error) && styles.error, className)}
styles.input,
styles.multiple,
Boolean(error) && styles.error,
className
)}
role="button" role="button"
tabIndex={0} tabIndex={0}
onKeyDown={onKeyDownHandler(() => { onKeyDown={onKeyDownHandler(() => {
@ -82,75 +96,79 @@ function InviteEmailsInput({
ref.current?.focus(); ref.current?.focus();
}} }}
> >
{values.map((option) => ( <div className={styles.wrapper}>
<Tag {values.map((option) => (
key={option.id} <Tag
variant="cell" key={option.id}
className={classNames( variant="cell"
styles.tag, className={classNames(
option.status && styles[option.status], styles.tag,
option.id === focusedValueId && styles.focused option.status && styles[option.status],
)} option.id === focusedValueId && styles.focused
onClick={() => { )}
onClick={() => {
ref.current?.focus();
}}
>
{option.value}
<IconButton
className={styles.delete}
size="small"
onClick={() => {
handleDelete(option);
}}
onKeyDown={onKeyDownHandler(() => {
handleDelete(option);
})}
>
<Close className={styles.close} />
</IconButton>
</Tag>
))}
<input
ref={ref}
placeholder={conditional(values.length === 0 && placeholder)}
value={currentValue}
style={{ minWidth: `${minInputWidth}px` }}
onKeyDown={(event) => {
if (event.key === 'Backspace' && currentValue === '') {
if (focusedValueId) {
onChange(values.filter(({ id }) => id !== focusedValueId));
setFocusedValueId(null);
} else {
setFocusedValueId(values.at(-1)?.id ?? null);
}
ref.current?.focus();
}
if (event.key === ' ' || event.code === 'Space' || event.key === 'Enter') {
// Focusing on input
if (currentValue !== '' && document.activeElement === ref.current) {
handleAdd(currentValue);
}
// Do not react to "Enter"
event.preventDefault();
}
}}
onChange={({ currentTarget: { value } }) => {
setCurrentValue(value);
setFocusedValueId(null);
}}
onFocus={() => {
ref.current?.focus(); ref.current?.focus();
}} }}
> onBlur={() => {
{option.value} if (currentValue !== '') {
<IconButton
className={styles.delete}
size="small"
onClick={() => {
handleDelete(option);
}}
onKeyDown={onKeyDownHandler(() => {
handleDelete(option);
})}
>
<Close className={styles.close} />
</IconButton>
</Tag>
))}
<input
ref={ref}
placeholder={conditional(values.length === 0 && placeholder)}
value={currentValue}
onKeyDown={(event) => {
if (event.key === 'Backspace' && currentValue === '') {
if (focusedValueId) {
onChange(values.filter(({ id }) => id !== focusedValueId));
setFocusedValueId(null);
} else {
setFocusedValueId(values.at(-1)?.id ?? null);
}
ref.current?.focus();
}
if (event.key === ' ' || event.code === 'Space' || event.key === 'Enter') {
// Focusing on input
if (currentValue !== '' && document.activeElement === ref.current) {
handleAdd(currentValue); handleAdd(currentValue);
} }
// Do not react to "Enter" setFocusedValueId(null);
event.preventDefault(); }}
} />
}} </div>
onChange={({ currentTarget: { value } }) => {
setCurrentValue(value);
setFocusedValueId(null);
}}
onFocus={() => {
ref.current?.focus();
}}
onBlur={() => {
if (currentValue !== '') {
handleAdd(currentValue);
}
setFocusedValueId(null);
}}
/>
</div> </div>
{Boolean(error) && typeof error === 'string' && ( {Boolean(error) && typeof error === 'string' && (
<div className={styles.errorMessage}>{error}</div> <div className={styles.errorMessage}>{error}</div>
)} )}
<canvas ref={canvasRef} />
</> </>
); );
} }