Skip to content

Commit

Permalink
refactor(console): improve invitation email input field (#5615)
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao authored Apr 2, 2024
1 parent e09318d commit d1c41a2
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
display: flex;
align-items: flex-start;
justify-content: space-between;
min-height: 96px;
padding: 0 _.unit(2) 0 _.unit(3);
min-height: 102px;
padding: _.unit(1.5) _.unit(3);
background: var(--color-layer-1);
border: 1px solid var(--color-border);
border-radius: 8px;
Expand All @@ -17,11 +17,12 @@
cursor: pointer;
position: relative;

&.multiple {
.wrapper {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: _.unit(2);
padding: _.unit(1.5) _.unit(3);
cursor: text;

.tag {
Expand Down Expand Up @@ -58,8 +59,7 @@
color: var(--color-text);
font: var(--font-body-2);
background: transparent;
flex-grow: 1;
padding: _.unit(0.5);
flex: 1;

&::placeholder {
color: var(--color-placeholder);
Expand All @@ -81,6 +81,10 @@
}
}

canvas {
display: none;
}

.errorMessage {
font: var(--font-body-2);
color: var(--color-error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { emailRegEx } from '@logto/core-kit';
import { generateStandardShortId } from '@logto/shared/universal';
import { conditional, type Nullable } from '@silverhand/essentials';
import classNames from 'classnames';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useFormContext } from 'react-hook-form';

import Close from '@/assets/icons/close.svg';
Expand All @@ -23,6 +23,13 @@ type Props = {
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({
className,
values,
Expand All @@ -35,6 +42,18 @@ function InviteEmailsInput({
const [currentValue, setCurrentValue] = useState('');
const { setError, clearErrors } = useFormContext<InviteMemberForm>();
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 { values: parsedValues, errorMessage } = parseEmailOptions(values);
Expand Down Expand Up @@ -67,12 +86,7 @@ function InviteEmailsInput({
return (
<>
<div
className={classNames(
styles.input,
styles.multiple,
Boolean(error) && styles.error,
className
)}
className={classNames(styles.input, Boolean(error) && styles.error, className)}
role="button"
tabIndex={0}
onKeyDown={onKeyDownHandler(() => {
Expand All @@ -82,75 +96,79 @@ function InviteEmailsInput({
ref.current?.focus();
}}
>
{values.map((option) => (
<Tag
key={option.id}
variant="cell"
className={classNames(
styles.tag,
option.status && styles[option.status],
option.id === focusedValueId && styles.focused
)}
onClick={() => {
ref.current?.focus();
}}
>
{option.value}
<IconButton
className={styles.delete}
size="small"
<div className={styles.wrapper}>
{values.map((option) => (
<Tag
key={option.id}
variant="cell"
className={classNames(
styles.tag,
option.status && styles[option.status],
option.id === focusedValueId && styles.focused
)}
onClick={() => {
handleDelete(option);
ref.current?.focus();
}}
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);
{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();
}
if (event.key === ' ' || event.code === 'Space' || event.key === 'Enter') {
// Focusing on input
if (currentValue !== '' && document.activeElement === ref.current) {
}}
onBlur={() => {
if (currentValue !== '') {
handleAdd(currentValue);
}
// Do not react to "Enter"
event.preventDefault();
}
}}
onChange={({ currentTarget: { value } }) => {
setCurrentValue(value);
setFocusedValueId(null);
}}
onFocus={() => {
ref.current?.focus();
}}
onBlur={() => {
if (currentValue !== '') {
handleAdd(currentValue);
}
setFocusedValueId(null);
}}
/>
setFocusedValueId(null);
}}
/>
</div>
</div>
{Boolean(error) && typeof error === 'string' && (
<div className={styles.errorMessage}>{error}</div>
)}
<canvas ref={canvasRef} />
</>
);
}
Expand Down

0 comments on commit d1c41a2

Please sign in to comment.