mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 02:43:50 +00:00
Feat/55 v2 (#225)
* chore: upgrade relevant libraries * feat(skip): add skip styling to paginated items * chore: upgrade relevant libraries * Fix: issue with text colour (#214) * fix: adjust text colour from context * fix: issue with wrong proptype * fix issue with vite migration (#217) * hotfix: 1.8.2 issues vite migration * fix: file import options * feat(55): styling * refactor: remove onhover option for entry block * refactor: relocate logging provider * refactor: convert to typescript * feat(55): add event editor * refactor: extract event actions * fix: bad import * feat(55): redesign components * fix: issues with not awaiting async * refactor: replace socket with subscription * refactor: remove unused * style: small tweaks * refactor: extract event actions * refactor: cleanup debug * refactor: chakra imports * fix: optimistic mutations * refactor: handle promise rejections * refactor: validation * fix: rq optimistic mutations * feat: add playback feedback to block * refactor: no optimistic adding of events * refactor: simplify cursor state * styles: cleanup editor style * refactor: cleanup duration update * fix: revert package upgrade (issues with vitest) * fix: prevent cyclic imports * refactor: extract data fetcher * refactor: typescript migration * chore: update tests
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
input[type="color"] {
|
||||
appearance: none;
|
||||
background-color: $action-blue;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Input } from '@chakra-ui/react';
|
||||
|
||||
import style from './ColourInput.module.scss';
|
||||
|
||||
interface ColourInputProps {
|
||||
value: string;
|
||||
handleChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
export default function ColourInput(props: ColourInputProps) {
|
||||
const { value, handleChange } = props;
|
||||
return (
|
||||
<Input
|
||||
size='sm'
|
||||
variant='filled'
|
||||
className={style.colourInput}
|
||||
type='color'
|
||||
value={value}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +1,86 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { clamp } from 'common/utils/math';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { clamp } from '../../utils/math';
|
||||
|
||||
import style from './TimeInput.module.css';
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
width: 20,
|
||||
fontWeight: 400,
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
backgroundColor: 'rgba(255,255,255,0.13)',
|
||||
color: '#fff',
|
||||
border: '1px solid #ecc94b55',
|
||||
borderRadius: '8px',
|
||||
variant: 'filled',
|
||||
borderRadius: '3px',
|
||||
placeholder: '-',
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
};
|
||||
|
||||
export default function DelayInput(props) {
|
||||
const { actionHandler, value } = props;
|
||||
const { submitHandler, value } = props;
|
||||
const [_value, setValue] = useState(value);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (value == null) return;
|
||||
setValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
/**
|
||||
* @description Prepare delay value for update
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validate = useCallback(
|
||||
(newValue) => {
|
||||
if (newValue === value) return;
|
||||
if (newValue === '') setValue(0);
|
||||
const delayValue = clamp(Number(newValue), -60, 60);
|
||||
|
||||
// convert to ms and updates
|
||||
const msVal = clamp(newValue, -60, 60) * 60000;
|
||||
actionHandler('update', { field: 'duration', value: msVal });
|
||||
if (delayValue === value) return;
|
||||
setValue(delayValue);
|
||||
|
||||
submitHandler(delayValue);
|
||||
},
|
||||
[actionHandler, value]
|
||||
[submitHandler, value]
|
||||
);
|
||||
|
||||
const labelText = `minutes ${value >= 0 ? 'delayed' : 'ahead'}`;
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback((event) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
validate(event.target.value);
|
||||
} else if (event.key === 'Escape') {
|
||||
inputRef.current.blur();
|
||||
setValue(value);
|
||||
}
|
||||
}, [validate, value]);
|
||||
|
||||
const labelText = `${Math.abs(value) > 1 ? 'minutes' : 'minute'} ${
|
||||
value >= 0 ? 'delayed' : 'ahead'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={style.timeInput}>
|
||||
<Editable
|
||||
<div className={style.delayInput}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-testid='delay-input'
|
||||
className={style.inputField}
|
||||
{...inputProps}
|
||||
value={_value}
|
||||
onChange={(v) => setValue(v)}
|
||||
onSubmit={(v) => handleSubmit(v)}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='number' min='-60' max='60' />
|
||||
</Editable>
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={() => setValue(value)}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
type='number'
|
||||
/>
|
||||
<span className={style.label}>{labelText}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DelayInput.propTypes = {
|
||||
actionHandler: PropTypes.func,
|
||||
submitHandler: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './EditableText.module.scss';
|
||||
|
||||
export default function EditableText(props) {
|
||||
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
|
||||
const [text, setText] = useState(defaultValue || '');
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue == null) setText('');
|
||||
else setText(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleSubmit = useCallback((submittedVal) => {
|
||||
// No need to update if it hasnt changed
|
||||
if (submittedVal === defaultValue) return;
|
||||
// submit a cleaned up version of the string
|
||||
const cleanVal = submittedVal.trim();
|
||||
submitHandler(cleanVal);
|
||||
|
||||
if (cleanVal !== submittedVal) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
},[defaultValue, submitHandler]);
|
||||
|
||||
const handleChange = useCallback((val) => {
|
||||
if (val.length < maxchar) setText(val);
|
||||
},[maxchar]);
|
||||
|
||||
return (
|
||||
<div className={style.block}>
|
||||
<span className={style.title}>{label}</span>
|
||||
<Editable
|
||||
onChange={(v) => handleChange(v)}
|
||||
onSubmit={(v) => handleSubmit(v)}
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
className={style.inline}
|
||||
{...rest}
|
||||
>
|
||||
<EditablePreview className={text === '' ? style.preview : ''} />
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EditableText.propTypes = {
|
||||
label: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
maxchar: PropTypes.number,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.block {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
padding-left: 1em;
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
display: inline-block;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.preview {
|
||||
color: $bg-gray-500;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LoggingContext } from '../../context/LoggingContext';
|
||||
import { forgivingStringToMillis } from '../../utils/dateConfig';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
|
||||
import style from './EditableTimer.module.scss';
|
||||
|
||||
export default function EditableTimer(props) {
|
||||
const { name, actionHandler, time = 0, delay, validate, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
|
||||
const handleSubmit = useCallback((value) => {
|
||||
// Check if there is anything there
|
||||
if (value === '') return false;
|
||||
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (value === 'p' || value === 'prev' || value === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
}
|
||||
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
|
||||
// string to pass should add to the end before
|
||||
const val = value.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(value);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validate(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
actionHandler('update', { field: name, value: newValMillis });
|
||||
|
||||
return true;
|
||||
},[actionHandler, delay, name, previousEnd, time, validate]);
|
||||
|
||||
// prepare time fields
|
||||
const validateValue = useCallback((value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(value);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
}
|
||||
},[delay, handleSubmit, time]);
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
return (
|
||||
<Editable
|
||||
data-testid='editable-timer'
|
||||
onChange={(v) => setValue(v)}
|
||||
onSubmit={(v) => validateValue(v)}
|
||||
onCancel={() => setValue(stringFromMillis(time + delay, true))}
|
||||
value={value}
|
||||
className={isDelayed ? style.delayedEditable : style.editable}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='text' placeholder='--:--:--' data-testid='editable-timer-input' />
|
||||
</Editable>
|
||||
);
|
||||
}
|
||||
|
||||
EditableTimer.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
validate: PropTypes.func.isRequired,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
@use '../../../theme/main'as *;
|
||||
|
||||
.editable,
|
||||
.delayedEditable {
|
||||
background-color: $input-bg;
|
||||
border: $input-border;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
border: $input-delayed-border;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Input, Textarea } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TextInput(props) {
|
||||
const { isTextArea, size = 'sm', field, initialText = '', submitHandler } = props;
|
||||
const inputRef = useRef(null);
|
||||
const [text, setText] = useState(initialText);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof initialText === 'undefined') {
|
||||
setText('');
|
||||
} else {
|
||||
setText(initialText);
|
||||
}
|
||||
}, [initialText]);
|
||||
|
||||
/**
|
||||
* @description Handles Input value change
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(newValue) => {
|
||||
if (newValue !== text) {
|
||||
setText(newValue);
|
||||
}
|
||||
},
|
||||
[text]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles submit events
|
||||
* @param {string} valueToSubmit
|
||||
*/
|
||||
const handleSubmit = useCallback(
|
||||
(valueToSubmit) => {
|
||||
// No need to update if it hasn't changed
|
||||
if (valueToSubmit === initialText) {
|
||||
return;
|
||||
}
|
||||
const cleanVal = valueToSubmit.trim();
|
||||
submitHandler(field, cleanVal);
|
||||
|
||||
if (cleanVal !== valueToSubmit) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
},
|
||||
[field, initialText, submitHandler]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Resets input value to given
|
||||
*/
|
||||
const resetValue = useCallback(async () => {
|
||||
setText(initialText);
|
||||
},[initialText])
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const keyHandler = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
resetValue();
|
||||
} else if (event.key === 'Enter') {
|
||||
if (!isTextArea) {
|
||||
handleSubmit(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
[resetValue, isTextArea, handleSubmit, text]
|
||||
);
|
||||
|
||||
return isTextArea ? (
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='filled'
|
||||
value={text}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
onBlur={(event) => handleSubmit(event.target.value)}
|
||||
onKeyDown={(event) => keyHandler(event)}
|
||||
data-testid='input-textarea'
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='filled'
|
||||
value={text}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
onBlur={(event) => handleSubmit(event.target.value)}
|
||||
onKeyDown={(event) => keyHandler(event)}
|
||||
data-testid='input-textfield'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TextInput.propTypes = {
|
||||
isTextArea: PropTypes.bool,
|
||||
size: PropTypes.string,
|
||||
field: PropTypes.string.isRequired,
|
||||
initialText: PropTypes.string,
|
||||
submitHandler: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { IconButton, Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
|
||||
import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
||||
import { LoggingContext } from 'common/context/LoggingContext';
|
||||
import { forgivingStringToMillis } from 'common/utils/dateConfig';
|
||||
import { stringFromMillis } from 'common/utils/time';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
export default function TimeInput(props) {
|
||||
const { name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const inputRef = useRef(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
/**
|
||||
* @description Resets input value to given
|
||||
*/
|
||||
const resetValue = useCallback(() => {
|
||||
// Todo: check if change is necessary
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [delay, emitError, time]);
|
||||
|
||||
/**
|
||||
* @description Selects input text on focus
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
inputRef.current.select();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description Submit handler
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleSubmit = useCallback(
|
||||
(newValue) => {
|
||||
// Check if there is anything there
|
||||
if (newValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
}
|
||||
} else if (
|
||||
newValue.startsWith('+') ||
|
||||
newValue.startsWith('p+') ||
|
||||
newValue.startsWith('p +')
|
||||
) {
|
||||
// string to pass should add to the end before
|
||||
const val = newValue.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(newValue);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validationHandler(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
submitHandler(name, newValMillis);
|
||||
|
||||
return true;
|
||||
},
|
||||
[delay, name, previousEnd, submitHandler, time, validationHandler]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Prepare time fields
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = useCallback(
|
||||
(newValue) => {
|
||||
const success = handleSubmit(newValue);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(newValue);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[delay, handleSubmit, resetValue]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
validateAndSubmit(event.target.value);
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit(event.target.value);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
inputRef.current.blur();
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[resetValue, validateAndSubmit]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
resetValue();
|
||||
}, [emitError, resetValue, time]);
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
|
||||
<InputLeftElement width='fit-content'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
icon={<IoLink style={{ transform: 'rotate(-45deg)' }} />}
|
||||
aria-label='automate'
|
||||
colorScheme='blue'
|
||||
style={{ borderRadius: '2px', width: 'min-content' }}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-testid='time-input'
|
||||
className={style.inputField}
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
variant='filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={resetValue}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
TimeInput.propTypes = {
|
||||
name: PropTypes.string,
|
||||
submitHandler: PropTypes.func,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
validationHandler: PropTypes.func,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
.timeInput {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-left: 0.8em;
|
||||
align-self: center;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
@mixin input-field {
|
||||
border: 1px solid transparent;
|
||||
background-color: $input-bg;
|
||||
font-size: 1em;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
background-color: $input-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.timeInput {
|
||||
width: fit-content !important;
|
||||
|
||||
.inputField {
|
||||
@include input-field;
|
||||
width: 7.5em;
|
||||
padding: 0 0 0 2.6em;
|
||||
}
|
||||
|
||||
&.delayed {
|
||||
.inputField {
|
||||
border: $input-delayed-border;
|
||||
background-color: $input-bg-delayed;
|
||||
|
||||
&:hover {
|
||||
background-color: $input-hover-bg-delayed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delayInput {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.inputField {
|
||||
@include input-field;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import TextInput from '../TextInput';
|
||||
|
||||
describe('TextInput component', () => {
|
||||
describe('when given props', () => {
|
||||
it('renders correctly', () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
render(<TextInput field={testField} initialText={testText} />);
|
||||
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(testText);
|
||||
});
|
||||
|
||||
it('Handles renders as textarea', () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
render(<TextInput field={testField} initialText={testText} isTextArea />);
|
||||
|
||||
const input = screen.getByTestId('input-textarea');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(testText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on status change', () => {
|
||||
it('calls submitHandler on new value', async () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
const myTypedString = '456';
|
||||
const expectedString = testText + myTypedString;
|
||||
const submitHandler = vi.fn();
|
||||
render(<TextInput field={testField} initialText={testText} submitHandler={submitHandler} />);
|
||||
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
|
||||
// submit without changing value
|
||||
await userEvent.type(input, '{enter}');
|
||||
expect(submitHandler).not.toHaveBeenCalled();
|
||||
|
||||
// on new value we can submit
|
||||
await userEvent.type(input, myTypedString);
|
||||
expect(input).toHaveValue(expectedString);
|
||||
await userEvent.type(input, '{enter}');
|
||||
expect(submitHandler).toHaveBeenCalledWith(testField, expectedString);
|
||||
});
|
||||
|
||||
it('cleans value before submitting', async () => {
|
||||
const testField = 'test';
|
||||
const myTypedString = ' 456 ';
|
||||
const expectedString = '456';
|
||||
const submitHandler = vi.fn();
|
||||
render(<TextInput field={testField} submitHandler={submitHandler} />);
|
||||
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
|
||||
// on new value we can submit
|
||||
await userEvent.type(input, myTypedString);
|
||||
expect(input).toHaveValue(myTypedString);
|
||||
await userEvent.type(input, '{enter}');
|
||||
expect(submitHandler).toHaveBeenCalledWith(testField, expectedString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles edge cases', () => {
|
||||
it('handles number values', () => {
|
||||
const testField = 'test';
|
||||
const testText = 123;
|
||||
render(<TextInput field={testField} initialText={testText} />);
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(`${testText}`);
|
||||
});
|
||||
it('handles null value', () => {
|
||||
const testField = 'test';
|
||||
const testText = null;
|
||||
const expected = '';
|
||||
render(<TextInput field={testField} initialText={testText} />);
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(expected);
|
||||
});
|
||||
it('handles undefined value', () => {
|
||||
const testField = 'test';
|
||||
const expected = '';
|
||||
render(<TextInput field={testField} />);
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user