Skip to content

Commit 63d9322

Browse files
committed
added allowTab to textarea, fixed recalcHeight on text area first load
1 parent 608879e commit 63d9322

File tree

4 files changed

+62
-24
lines changed

4 files changed

+62
-24
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"dependencies": {
6161
"@fluentui/react-datepicker-compat": "^0.4.53",
6262
"@fluentui/react-timepicker-compat": "^0.2.42",
63-
"@kwiz/common": "^1.0.103",
63+
"@kwiz/common": "^1.0.120",
6464
"@mismerge/core": "^1.2.1",
6565
"@mismerge/react": "^1.0.1",
6666
"esbuild": "^0.19.12",

src/controls/field-editor.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Field, mergeClasses, Textarea } from '@fluentui/react-components';
1+
import { Field, mergeClasses } from '@fluentui/react-components';
22
import { isNullOrUndefined } from '@kwiz/common';
33
import React from 'react';
44
import { GetLogger } from '../_modules/config';
5-
import { InputEx } from './input';
5+
import { InputEx, TextAreaEx } from './input';
66

77
const logger = GetLogger('FieldEditor');
88

@@ -14,7 +14,8 @@ interface IProps {
1414
css: string[];
1515
label: string;
1616
description?: string;
17-
type?: "text" | "multiline"
17+
type?: "text" | "multiline";
18+
allowTab?: boolean;
1819
}
1920
export const FieldEditor: React.FunctionComponent<IProps> = (props) => {
2021
if (isNullOrUndefined(props.value)) {
@@ -25,11 +26,12 @@ export const FieldEditor: React.FunctionComponent<IProps> = (props) => {
2526
validationMessage={props.error || props.description}
2627
validationState={props.error ? "error" : "none"}>
2728
{props.type === "multiline"
28-
? <Textarea className={props.css && mergeClasses(...props.css)}
29+
? <TextAreaEx className={props.css && mergeClasses(...props.css)}
2930
required={props.required}
3031
placeholder={props.label}
3132
value={props.value || ""}
32-
onChange={(e, data) => props.onChange(data.value)}
33+
allowTab={props.allowTab}
34+
onValueChange={(e, data) => props.onChange(data.value)}
3335
/>
3436
: <InputEx className={props.css && mergeClasses(...props.css)}
3537
required={props.required}

src/controls/input.tsx

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { GriffelStyle, Input, InputOnChangeData, InputProps, Label, Link, makeStyles, mergeClasses, Textarea, TextareaProps } from '@fluentui/react-components';
2-
import { isFunction, isNotEmptyArray, isNullOrEmptyString, isNullOrNaN, isNullOrUndefined, isNumber } from '@kwiz/common';
3-
import React from 'react';
1+
import { GriffelStyle, Input, InputOnChangeData, InputProps, Label, Link, makeStyles, mergeClasses, Textarea, TextareaOnChangeData, TextareaProps } from '@fluentui/react-components';
2+
import { isFunction, isNotEmptyArray, isNullOrEmptyString, isNullOrNaN, isNullOrUndefined, isNumber, pasteTextAtCursor, stopEvent } from '@kwiz/common';
3+
import React, { useCallback, useEffect } from 'react';
4+
import { useEffectOnlyOnMount, useRefWithState } from '../helpers';
45
import { useKWIZFluentContext } from '../helpers/context-internal';
56
import { useCommonStyles } from '../styles/styles';
67
import { Horizontal } from './horizontal';
@@ -10,7 +11,9 @@ import { Vertical } from './vertical';
1011

1112

1213
interface IProps extends InputProps {
14+
/** fire on enter */
1315
onOK?: () => void;
16+
/** fire on escape */
1417
onCancel?: () => void;
1518
tokens?: { title: string; value: string; replace?: boolean; }[];
1619
tokenMenuLabel?: string;
@@ -20,8 +23,8 @@ export const InputEx: React.FunctionComponent<React.PropsWithChildren<IProps>> =
2023
const input = <Input appearance={ctx.inputAppearance} {...props}
2124
onKeyDown={isFunction(props.onOK) || isFunction(props.onCancel)
2225
? e => {
23-
if (isFunction(props.onOK) && e.key === "Enter") props.onOK();
24-
else if (isFunction(props.onCancel) && e.key === "Escape") props.onCancel();
26+
if (e.key === "Enter") props.onOK?.();
27+
if (e.key === "Escape") props.onCancel?.();
2528
}
2629
: undefined
2730
}
@@ -69,27 +72,60 @@ const useStyles = makeStyles({
6972

7073
interface IPropsTextArea extends TextareaProps {
7174
fullSize?: boolean;
75+
/** recalc the height to grow to show all text */
7276
growNoShrink?: boolean;
77+
allowTab?: boolean;
78+
/** fire on enter */
79+
onOK?: () => void;
80+
/** fire on escape */
81+
onCancel?: () => void;
82+
onValueChange?: (e: React.ChangeEvent<HTMLTextAreaElement> | React.KeyboardEvent<HTMLTextAreaElement>, d: {
83+
value: string;
84+
elm: HTMLTextAreaElement;
85+
}) => void;
7386
}
7487
export const TextAreaEx: React.FunctionComponent<React.PropsWithChildren<IPropsTextArea>> = (props) => {
7588
const cssNames = useStyles();
7689
let css: string[] = [];
7790

7891
if (props.fullSize) css.push(cssNames.fullSizeTextArea);
79-
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
92+
const textAreaRef = useRefWithState<HTMLTextAreaElement>(null);
8093
const recalcHeight = React.useCallback(() => {
81-
if (textAreaRef.current && props.growNoShrink) {
82-
if (textAreaRef.current.scrollHeight > textAreaRef.current.clientHeight)
83-
textAreaRef.current.style.minHeight = textAreaRef.current.scrollHeight + 'px';
94+
if (textAreaRef.ref.current && props.growNoShrink) {
95+
if (textAreaRef.ref.current.scrollHeight > textAreaRef.ref.current.clientHeight)
96+
textAreaRef.ref.current.style.minHeight = textAreaRef.ref.current.scrollHeight + 'px';
8497
}
85-
}, [textAreaRef]);
98+
}, useEffectOnlyOnMount);
99+
100+
useEffect(() => { recalcHeight(); }, [textAreaRef.value]);
101+
102+
const onChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement> | React.KeyboardEvent<HTMLTextAreaElement>, d: TextareaOnChangeData) => {
103+
props.onValueChange?.(e, { value: d.value, elm: textAreaRef.ref.current });
104+
recalcHeight();
105+
}, [props.onChange]);
106+
107+
const needOnKeyDown = props.allowTab || isFunction(props.onOK) || isFunction(props.onCancel);
86108

87109
let style: React.CSSProperties = { height: '100%', ...props.style };
110+
88111
return (
89-
<Textarea ref={textAreaRef} className={mergeClasses(...css)} {...props} style={style} onChange={(e, d) => {
90-
if (props.onChange) props.onChange(e, d);
91-
recalcHeight();
92-
}} />
112+
<Textarea ref={textAreaRef.set} className={mergeClasses(...css)} {...props} style={style}
113+
onKeyDown={needOnKeyDown ? (e) => {
114+
if (props.allowTab && e.key === "Tab") {
115+
stopEvent(e);
116+
const textArea = e.target as HTMLTextAreaElement;
117+
pasteTextAtCursor(textArea, "\t");
118+
onChange(e, { value: textArea.value });
119+
return;
120+
}
121+
if (e.key === "Enter") props.onOK?.();
122+
if (e.key === "Escape") props.onCancel?.();
123+
props.onKeyDown?.(e);
124+
} : props.onKeyDown}
125+
onChange={(e, d) => {
126+
props.onChange?.(e, d);
127+
onChange(e, d);
128+
}} />
93129
);
94130
}
95131

0 commit comments

Comments
 (0)