LCOV - code coverage report
Current view: top level - dashql-app/src/ui/foundations - text_field.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 96.7 % 121 117
Test Date: 2026-11-06 19:30:32

            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 : }
        

Generated by: LCOV version 2.0-1