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:
parent
e09318d3e8
commit
d1c41a2fa7
2 changed files with 96 additions and 74 deletions
|
@ -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);
|
||||||
|
|
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue