Line data Source code
1 : import * as React from 'react';
2 :
3 : import { classNames } from '../../utils/classnames.js';
4 : import { VariantKind } from '../../utils/variant.js';
5 : import { TextInput, TextInputValidationStatus } from './text_input.js';
6 : import { CopyToClipboardAction } from './text_input_action.js';
7 :
8 : import icons from '@ankoh/dashql-svg-symbols';
9 : import * as styles from './text_field.module.css';
10 :
11 1 : export const VALIDATION_UNKNOWN = Symbol("VALIDATION_UNKNOWN");
12 1 : export const VALIDATION_OK = Symbol("VALIDATION_OK");
13 1 : export const VALIDATION_WARNING = Symbol("VALIDATION_WARNING");
14 1 : export const VALIDATION_ERROR = Symbol("VALIDATION_ERROR");
15 :
16 : export type TextFieldValidationStatus =
17 : | VariantKind<typeof VALIDATION_UNKNOWN, null>
18 : | VariantKind<typeof VALIDATION_OK, null>
19 : | VariantKind<typeof VALIDATION_WARNING, string>
20 : | VariantKind<typeof VALIDATION_ERROR, string>;
21 :
22 1 : function TextFieldValidation(props: { id: string; validation?: TextFieldValidationStatus }) {
23 1 : if (!props.validation) {
24 1 : return undefined;
25 : }
26 1 : switch (props.validation.type) {
27 0 : case VALIDATION_OK:
28 1 : case VALIDATION_UNKNOWN: {
29 1 : return undefined;
30 : }
31 1 : case VALIDATION_WARNING: {
32 1 : return (
33 1 : <div id={props.id} className={styles.text_field_validation_warning}>
34 1 : <svg className={styles.text_field_validation_icon} width="12px" height="12px">
35 1 : <use xlinkHref={`${icons}#alert_fill_12`} />
36 1 : </svg>
37 1 : <span className={styles.text_field_validation_text}>
38 1 : {props.validation.value}
39 1 : </span>
40 1 : </div>
41 : );
42 : }
43 1 : case VALIDATION_ERROR: {
44 1 : return (
45 1 : <div id={props.id} className={styles.text_field_validation_error}>
46 1 : <svg className={styles.text_field_validation_icon} width="12px" height="12px">
47 1 : <use xlinkHref={`${icons}#alert_fill_12`} />
48 1 : </svg>
49 1 : <span className={styles.text_field_validation_text}>
50 1 : {props.validation.value}
51 1 : </span>
52 1 : </div>
53 : );
54 : }
55 :
56 1 : }
57 1 : }
58 :
59 1 : export function TextField(props: {
60 : inputRef?: React.Ref<HTMLInputElement>;
61 : className?: string;
62 : name?: string;
63 : caption?: string;
64 : value: string;
65 : placeholder?: string;
66 : leadingVisual?: React.ElementType;
67 : onChange?: React.ChangeEventHandler<HTMLInputElement>;
68 : disabled?: boolean;
69 : concealed?: boolean;
70 : readOnly?: boolean;
71 : validation?: TextFieldValidationStatus;
72 : logContext: string;
73 : autoComplete?: boolean;
74 : style?: React.CSSProperties;
75 : /// The test id of the input element
76 : 'data-testid'?: string;
77 1 : }) {
78 1 : const validationId = React.useId();
79 1 : let validationStatus: undefined | TextInputValidationStatus = undefined;
80 1 : if (props.validation?.type === VALIDATION_OK) {
81 0 : validationStatus = TextInputValidationStatus.Success;
82 1 : } else if (props.validation?.type === VALIDATION_WARNING) {
83 1 : validationStatus = TextInputValidationStatus.Warning;
84 1 : } else if (props.validation?.type === VALIDATION_ERROR) {
85 1 : validationStatus = TextInputValidationStatus.Error;
86 : }
87 1 : const hasValidationMessage = props.validation?.type === VALIDATION_WARNING || props.validation?.type === VALIDATION_ERROR;
88 1 : return (
89 1 : <div className={classNames(styles.text_field, props.className)} style={props.style}>
90 1 : {props.name && <div className={styles.text_field_name}>{props.name}</div>}
91 1 : {props.caption && <div className={styles.text_field_caption}>{props.caption}</div>}
92 1 : <TextInput
93 1 : ref={props.inputRef}
94 1 : className={classNames(styles.text_field_input, {
95 1 : [styles.text_field_disabled]: props.disabled
96 1 : })}
97 1 : block
98 1 : placeholder={props.placeholder}
99 1 : leadingVisual={props.leadingVisual}
100 1 : trailingAction={<CopyToClipboardAction value={props.value} logContext={props.logContext} ariaLabel={`Copy ${props.name}`} />}
101 : // A concealed field is a password input with the real value, so typing edits the value and not its mask
102 1 : type={props.concealed ? 'password' : undefined}
103 1 : value={props.value}
104 1 : onChange={props.onChange}
105 1 : disabled={props.disabled}
106 1 : readOnly={props.readOnly}
107 1 : aria-label={props.name}
108 1 : aria-invalid={validationStatus === TextInputValidationStatus.Error}
109 1 : aria-describedby={hasValidationMessage ? validationId : undefined}
110 1 : autoComplete={props.autoComplete}
111 1 : validationStatus={validationStatus}
112 1 : data-testid={props['data-testid']}
113 1 : />
114 1 : <TextFieldValidation id={validationId} validation={props.validation} />
115 1 : </div>
116 : );
117 1 : }
118 :
119 1 : export function KeyValueTextField(props: {
120 : className?: string;
121 : name: string;
122 : caption?: string;
123 : k: string;
124 : v: string;
125 : keyPlaceholder: string;
126 : valuePlaceholder: string;
127 : keyIcon?: React.ElementType;
128 : valueIcon?: React.ElementType;
129 : onChangeKey?: React.ChangeEventHandler<HTMLInputElement>;
130 : onChangeValue?: React.ChangeEventHandler<HTMLInputElement>;
131 : disabled?: boolean;
132 : readOnly?: boolean;
133 : validation?: TextFieldValidationStatus;
134 : keyAriaLabel: string;
135 : valueAriaLabel: string;
136 : logContext: string;
137 1 : }) {
138 1 : const validationId = React.useId();
139 1 : let validationStatus: undefined | TextInputValidationStatus = undefined;
140 1 : if (props.validation?.type === VALIDATION_OK) {
141 0 : validationStatus = TextInputValidationStatus.Success;
142 1 : } else if (props.validation?.type === VALIDATION_WARNING) {
143 0 : validationStatus = TextInputValidationStatus.Warning;
144 1 : } else if (props.validation?.type === VALIDATION_ERROR) {
145 1 : validationStatus = TextInputValidationStatus.Error;
146 : }
147 1 : const hasValidationMessage = props.validation?.type === VALIDATION_WARNING || props.validation?.type === VALIDATION_ERROR;
148 1 : return (
149 1 : <div className={classNames(styles.kv_field, props.className)}>
150 1 : <div className={styles.kv_field_name}>{props.name}</div>
151 1 : {props.caption && <div className={styles.kv_field_caption}>{props.caption}</div>}
152 1 : <TextInput
153 1 : className={styles.kv_field_input_key}
154 1 : block
155 1 : value={props.k}
156 1 : placeholder={props.keyPlaceholder}
157 1 : leadingVisual={props.keyIcon}
158 1 : trailingAction={<CopyToClipboardAction value={props.k} logContext={props.logContext} ariaLabel={`Copy ${props.keyAriaLabel}`} />}
159 1 : onChange={props.onChangeKey}
160 1 : readOnly={props.readOnly}
161 1 : disabled={props.disabled}
162 1 : aria-label={props.keyAriaLabel}
163 1 : aria-invalid={validationStatus === TextInputValidationStatus.Error}
164 1 : aria-describedby={hasValidationMessage ? validationId : undefined}
165 1 : validationStatus={validationStatus}
166 1 : />
167 1 : <div className={styles.kv_field_input_value}>
168 1 : <TextInput
169 1 : block
170 1 : value={props.v}
171 1 : placeholder={props.valuePlaceholder}
172 1 : leadingVisual={props.valueIcon}
173 1 : trailingAction={<CopyToClipboardAction value={props.v} logContext={props.logContext} ariaLabel={`Copy ${props.valueAriaLabel}`} />}
174 1 : onChange={props.onChangeValue}
175 1 : readOnly={props.readOnly}
176 1 : disabled={props.disabled}
177 1 : aria-label={props.valueAriaLabel}
178 1 : aria-invalid={validationStatus === TextInputValidationStatus.Error}
179 1 : aria-describedby={hasValidationMessage ? validationId : undefined}
180 1 : validationStatus={validationStatus}
181 1 : />
182 1 : </div>
183 1 : <TextFieldValidation id={validationId} validation={props.validation} />
184 1 : </div>
185 : );
186 1 : }
|