Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03a70d27a3 |
@@ -63,11 +63,16 @@ function diffChars(prev: ReadonlyArray<{ id: number; char: string }>, next: stri
|
|||||||
return { reusedId, keptPrev };
|
return { reusedId, keptPrev };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Browsers render password glyphs as this bullet; match it in the overlay
|
||||||
|
* so animated fields don't leak the real characters visually. */
|
||||||
|
const PASSWORD_MASK_CHAR = '•';
|
||||||
|
|
||||||
function useFieldAnimation(
|
function useFieldAnimation(
|
||||||
multiline: boolean,
|
multiline: boolean,
|
||||||
externalRef: Ref<FieldElement>,
|
externalRef: Ref<FieldElement>,
|
||||||
controlledValue: ComponentPropsWithoutRef<'input'>['value'],
|
controlledValue: ComponentPropsWithoutRef<'input'>['value'],
|
||||||
initial: ComponentPropsWithoutRef<'input'>['defaultValue'],
|
initial: ComponentPropsWithoutRef<'input'>['defaultValue'],
|
||||||
|
masked = false,
|
||||||
) {
|
) {
|
||||||
const innerRef = useRef<FieldElement | null>(null);
|
const innerRef = useRef<FieldElement | null>(null);
|
||||||
const overlayRef = useRef<HTMLDivElement | null>(null);
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -144,33 +149,34 @@ function useFieldAnimation(
|
|||||||
|
|
||||||
const overlay = (
|
const overlay = (
|
||||||
<div
|
<div
|
||||||
ref={overlayRef}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cx('modern-sk-field-overlay', multiline && 'modern-sk-field-overlay--multiline')}
|
className={cx('modern-sk-field-overlay', multiline && 'modern-sk-field-overlay--multiline')}
|
||||||
>
|
>
|
||||||
{entries.map((e) =>
|
<div ref={overlayRef} className="modern-sk-field-overlay-content">
|
||||||
e.leaving ? (
|
{entries.map((e) =>
|
||||||
<span
|
e.leaving ? (
|
||||||
key={`${idPrefix}-${e.id}`}
|
<span
|
||||||
className="modern-sk-field-char modern-sk-field-char--leaving"
|
key={`${idPrefix}-${e.id}`}
|
||||||
style={{ left: e.x, top: e.y }}
|
className="modern-sk-field-char modern-sk-field-char--leaving"
|
||||||
onAnimationEnd={() => onLeaveEnd(e.id)}
|
style={{ left: e.x, top: e.y }}
|
||||||
>
|
onAnimationEnd={() => onLeaveEnd(e.id)}
|
||||||
{e.char}
|
>
|
||||||
</span>
|
{masked ? PASSWORD_MASK_CHAR : e.char}
|
||||||
) : (
|
</span>
|
||||||
<span
|
) : (
|
||||||
key={`${idPrefix}-${e.id}`}
|
<span
|
||||||
ref={(node) => {
|
key={`${idPrefix}-${e.id}`}
|
||||||
if (node) spanRefs.current.set(e.id, node);
|
ref={(node) => {
|
||||||
else spanRefs.current.delete(e.id);
|
if (node) spanRefs.current.set(e.id, node);
|
||||||
}}
|
else spanRefs.current.delete(e.id);
|
||||||
className={cx('modern-sk-field-char', !multiline && 'modern-sk-field-char--composited')}
|
}}
|
||||||
>
|
className={cx('modern-sk-field-char', !multiline && 'modern-sk-field-char--composited')}
|
||||||
{e.char}
|
>
|
||||||
</span>
|
{masked ? PASSWORD_MASK_CHAR : e.char}
|
||||||
),
|
</span>
|
||||||
)}
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -187,6 +193,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||||||
ref,
|
ref,
|
||||||
props.value,
|
props.value,
|
||||||
props.defaultValue,
|
props.defaultValue,
|
||||||
|
props.type === 'password',
|
||||||
);
|
);
|
||||||
if (!animated) {
|
if (!animated) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export const Playground: Story = {};
|
|||||||
export const Disabled: Story = { args: { disabled: true, value: 'Read only' } };
|
export const Disabled: Story = { args: { disabled: true, value: 'Read only' } };
|
||||||
|
|
||||||
export const Multiline: Story = {
|
export const Multiline: Story = {
|
||||||
render: () => <TextArea rows={4} placeholder="Multiple lines…" style={{ width: 320 }} />,
|
render: () => (
|
||||||
|
<TextArea rows={4} placeholder="Multiple lines…" style={{ width: 320 }} />
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Search: Story = {
|
export const Search: Story = {
|
||||||
@@ -46,3 +48,5 @@ export const Search: Story = {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Password: Story = { args: { type: 'password', defaultValue: 'hi!' } };
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ textarea.modern-sk-field {
|
|||||||
.modern-sk-search .ph {
|
.modern-sk-search .ph {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 11px;
|
left: 11px;
|
||||||
|
z-index: 1;
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -255,13 +256,19 @@ textarea.modern-sk-field {
|
|||||||
color: var(--fg-1);
|
color: var(--fg-1);
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
will-change: transform;
|
|
||||||
}
|
}
|
||||||
.modern-sk-field-overlay--multiline {
|
.modern-sk-field-overlay--multiline {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
/* Scroll-sync transform lives on this inner div, not the clipping parent above —
|
||||||
|
transforming the overflow:hidden box itself would drag its clip region along
|
||||||
|
with the content and let overflowing lines spill outside the field's border. */
|
||||||
|
.modern-sk-field-overlay-content {
|
||||||
|
position: relative;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
.modern-sk-search .modern-sk-field-overlay {
|
.modern-sk-search .modern-sk-field-overlay {
|
||||||
padding-left: 34px;
|
padding-left: 34px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user