guestbell-forms 3.0.102 → 3.0.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/base/input/useBaseInput.d.ts +44 -0
- package/build/components/base/input/useBaseInput.js +181 -0
- package/build/components/base/input/useBaseInput.js.map +1 -0
- package/build/components/tags/Tags.d.ts +1 -1
- package/build/components/tags/Tags.js +19 -11
- package/build/components/tags/Tags.js.map +1 -1
- package/build/dist/guestbell-forms.css +9 -10
- package/build/dist/guestbell-forms.css.map +1 -1
- package/build/dist/guestbell-forms.min.css +1 -1
- package/build/dist/report.html +2 -2
- package/build/scss/components/tags/tags.scss +9 -8
- package/package.json +1 -1
- package/src/lib/components/base/input/useBaseInput.tsx +239 -0
- package/src/lib/components/tags/Tags.tsx +32 -14
- package/src/lib/scss/components/tags/tags.scss +9 -8
- package/src/stories/TagsDemo.tsx +9 -5
@@ -0,0 +1,239 @@
|
|
1
|
+
// experimental and unfinished and unused for now
|
2
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
3
|
+
import {
|
4
|
+
AllowedHtmlElements,
|
5
|
+
BaseTranslations,
|
6
|
+
ValidationError,
|
7
|
+
} from './BaseInput';
|
8
|
+
import * as Validators from '../../../validators';
|
9
|
+
import { FormContextState } from '../../form/FormContext';
|
10
|
+
import guid from '../../utils/Guid';
|
11
|
+
|
12
|
+
interface UseBaseInputProps<TranslationsT extends BaseTranslations> {
|
13
|
+
value?: string;
|
14
|
+
required?: boolean;
|
15
|
+
validators?: string[];
|
16
|
+
customValidators?: Validators.IBaseValidator[];
|
17
|
+
onTheFlightValidate?: (value: string) => boolean;
|
18
|
+
onChange?: (
|
19
|
+
event: React.ChangeEvent<AllowedHtmlElements>,
|
20
|
+
isValid: boolean
|
21
|
+
) => void;
|
22
|
+
onBlur?: () => void;
|
23
|
+
onFocus?: () => void;
|
24
|
+
errors?: ValidationError[];
|
25
|
+
infoText?: string;
|
26
|
+
touchOn?: 'focus' | 'blur';
|
27
|
+
showValidation?: boolean;
|
28
|
+
disabled?: boolean;
|
29
|
+
ignoreContext?: boolean;
|
30
|
+
formContext?: FormContextState;
|
31
|
+
translations?: TranslationsT;
|
32
|
+
validationName?: string;
|
33
|
+
label?: string | JSX.Element;
|
34
|
+
}
|
35
|
+
|
36
|
+
export function useBaseInput<
|
37
|
+
TranslationsT extends BaseTranslations = BaseTranslations
|
38
|
+
>(props: UseBaseInputProps<TranslationsT>) {
|
39
|
+
const {
|
40
|
+
value: initialValue = '',
|
41
|
+
required = false,
|
42
|
+
validators = [],
|
43
|
+
customValidators = [],
|
44
|
+
onTheFlightValidate,
|
45
|
+
onChange,
|
46
|
+
onBlur,
|
47
|
+
onFocus,
|
48
|
+
//errors: propErrors = [],
|
49
|
+
//infoText,
|
50
|
+
touchOn = 'focus',
|
51
|
+
//showValidation = true,
|
52
|
+
disabled = false,
|
53
|
+
ignoreContext = false,
|
54
|
+
formContext,
|
55
|
+
translations,
|
56
|
+
validationName,
|
57
|
+
label,
|
58
|
+
} = props;
|
59
|
+
|
60
|
+
const [value, setValue] = useState<string>(initialValue);
|
61
|
+
const [touched, setTouched] = useState<boolean>(false);
|
62
|
+
const [isValid, setIsValid] = useState<boolean>(true);
|
63
|
+
const [errors, setErrors] = useState<ValidationError[]>([]);
|
64
|
+
const [focused, setFocused] = useState<boolean>(false);
|
65
|
+
const [componentDisabled, setComponentDisabled] = useState<boolean>(disabled);
|
66
|
+
|
67
|
+
const inputRef = useRef<AllowedHtmlElements>(null);
|
68
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
69
|
+
const componentId = useRef(guid());
|
70
|
+
|
71
|
+
const handleValidation = useCallback(
|
72
|
+
(currentValue: string, initializing = false) => {
|
73
|
+
let validationErrors: ValidationError[] = [];
|
74
|
+
let validationStatus = true;
|
75
|
+
|
76
|
+
if (required && !currentValue) {
|
77
|
+
validationErrors.push(
|
78
|
+
translations?.required || 'This field is required'
|
79
|
+
);
|
80
|
+
validationStatus = false;
|
81
|
+
} else if (validators) {
|
82
|
+
validators.forEach((validator) => {
|
83
|
+
let isValidForValidator = false;
|
84
|
+
switch (validator) {
|
85
|
+
case 'email':
|
86
|
+
isValidForValidator = new Validators.EmailValidator().Validate(
|
87
|
+
currentValue,
|
88
|
+
required,
|
89
|
+
(error) => validationErrors.push(error)
|
90
|
+
);
|
91
|
+
break;
|
92
|
+
// Add other validators as needed
|
93
|
+
default:
|
94
|
+
throw new Error(`Validator ${validator} not implemented`);
|
95
|
+
}
|
96
|
+
if (!isValidForValidator) validationStatus = false;
|
97
|
+
});
|
98
|
+
}
|
99
|
+
|
100
|
+
customValidators.forEach((customValidator) => {
|
101
|
+
const isValidForCustomValidator = customValidator.Validate(
|
102
|
+
currentValue,
|
103
|
+
required,
|
104
|
+
(error) => validationErrors.push(error)
|
105
|
+
);
|
106
|
+
if (!isValidForCustomValidator) validationStatus = false;
|
107
|
+
});
|
108
|
+
|
109
|
+
setErrors(validationErrors);
|
110
|
+
setIsValid(validationStatus);
|
111
|
+
|
112
|
+
if (!initializing && !ignoreContext) {
|
113
|
+
formContext?.updateCallback(componentId.current, {
|
114
|
+
isValid: validationStatus,
|
115
|
+
errors: validationErrors,
|
116
|
+
});
|
117
|
+
}
|
118
|
+
|
119
|
+
return { isValid: validationStatus, errors: validationErrors };
|
120
|
+
},
|
121
|
+
[
|
122
|
+
required,
|
123
|
+
validators,
|
124
|
+
customValidators,
|
125
|
+
formContext,
|
126
|
+
translations,
|
127
|
+
ignoreContext,
|
128
|
+
]
|
129
|
+
);
|
130
|
+
|
131
|
+
const handleValueChange = useCallback(
|
132
|
+
(newValue: string) => {
|
133
|
+
if (!onTheFlightValidate || onTheFlightValidate(newValue)) {
|
134
|
+
const result = handleValidation(newValue);
|
135
|
+
setValue(newValue);
|
136
|
+
onChange?.(
|
137
|
+
{
|
138
|
+
target: { value: newValue },
|
139
|
+
} as React.ChangeEvent<AllowedHtmlElements>,
|
140
|
+
result.isValid
|
141
|
+
);
|
142
|
+
}
|
143
|
+
},
|
144
|
+
[handleValidation, onTheFlightValidate, onChange]
|
145
|
+
);
|
146
|
+
|
147
|
+
const handleFocus = useCallback(() => {
|
148
|
+
if (!disabled) {
|
149
|
+
onFocus?.();
|
150
|
+
setFocused(true);
|
151
|
+
if (touchOn === 'focus') setTouched(true);
|
152
|
+
}
|
153
|
+
}, [disabled, onFocus, touchOn]);
|
154
|
+
|
155
|
+
const handleBlur = useCallback(() => {
|
156
|
+
onBlur?.();
|
157
|
+
setFocused(false);
|
158
|
+
if (touchOn === 'blur') setTouched(true);
|
159
|
+
}, [onBlur, touchOn]);
|
160
|
+
|
161
|
+
const scrollTo = useCallback(() => {
|
162
|
+
containerRef.current?.scrollIntoView({ behavior: 'smooth' });
|
163
|
+
}, []);
|
164
|
+
|
165
|
+
const setValid = useCallback(() => {
|
166
|
+
setIsValid(true);
|
167
|
+
setErrors([]);
|
168
|
+
formContext?.updateCallback(componentId.current, {
|
169
|
+
isValid: true,
|
170
|
+
errors: [],
|
171
|
+
});
|
172
|
+
}, [formContext]);
|
173
|
+
|
174
|
+
const setInvalid = useCallback(
|
175
|
+
(validationErrors: ValidationError[] = []) => {
|
176
|
+
setIsValid(false);
|
177
|
+
setErrors(validationErrors);
|
178
|
+
formContext?.updateCallback(componentId.current, {
|
179
|
+
isValid: false,
|
180
|
+
errors: validationErrors,
|
181
|
+
});
|
182
|
+
},
|
183
|
+
[formContext]
|
184
|
+
);
|
185
|
+
|
186
|
+
const disableComponent = useCallback(() => {
|
187
|
+
setComponentDisabled(true);
|
188
|
+
}, []);
|
189
|
+
|
190
|
+
const enableComponent = useCallback(() => {
|
191
|
+
setComponentDisabled(false);
|
192
|
+
}, []);
|
193
|
+
|
194
|
+
useEffect(() => {
|
195
|
+
handleValidation(value, true);
|
196
|
+
|
197
|
+
if (!ignoreContext && formContext) {
|
198
|
+
formContext.subscribe(componentId.current, {
|
199
|
+
componentApi: {
|
200
|
+
disableComponent,
|
201
|
+
enableComponent,
|
202
|
+
focus: () => inputRef.current?.focus(),
|
203
|
+
scrollTo,
|
204
|
+
touch: () => setTouched(true),
|
205
|
+
unTouch: () => setTouched(false),
|
206
|
+
},
|
207
|
+
validation: {
|
208
|
+
isValid,
|
209
|
+
errors,
|
210
|
+
name: validationName || label,
|
211
|
+
},
|
212
|
+
});
|
213
|
+
}
|
214
|
+
|
215
|
+
return () => {
|
216
|
+
if (!ignoreContext) formContext?.unSubscribe(componentId.current);
|
217
|
+
};
|
218
|
+
}, [formContext, ignoreContext, handleValidation, value]);
|
219
|
+
|
220
|
+
return {
|
221
|
+
value,
|
222
|
+
setValue: handleValueChange,
|
223
|
+
isValid,
|
224
|
+
errors,
|
225
|
+
touched,
|
226
|
+
focused,
|
227
|
+
inputRef,
|
228
|
+
containerRef,
|
229
|
+
handleFocus,
|
230
|
+
handleBlur,
|
231
|
+
scrollTo,
|
232
|
+
setValid,
|
233
|
+
setInvalid,
|
234
|
+
disableComponent,
|
235
|
+
enableComponent,
|
236
|
+
setTouched,
|
237
|
+
componentDisabled,
|
238
|
+
};
|
239
|
+
}
|
@@ -176,7 +176,10 @@ export class TagsRaw<
|
|
176
176
|
}
|
177
177
|
}
|
178
178
|
|
179
|
-
public componentDidUpdate(
|
179
|
+
public componentDidUpdate(
|
180
|
+
oldProps: TagsProps<IdT, T> & InjectedProps,
|
181
|
+
oldState: TagsState<IdT, T>
|
182
|
+
) {
|
180
183
|
if (
|
181
184
|
oldProps.tags !== this.props.tags ||
|
182
185
|
oldProps.validators !== this.props.validators ||
|
@@ -185,6 +188,15 @@ export class TagsRaw<
|
|
185
188
|
) {
|
186
189
|
this.handleErrors(this.props.tags);
|
187
190
|
}
|
191
|
+
if (
|
192
|
+
this.state.suggestionsVisible &&
|
193
|
+
(oldProps.tags !== this.props.tags ||
|
194
|
+
oldProps.existingTags !== this.props.existingTags ||
|
195
|
+
this.state.value !== oldState.value ||
|
196
|
+
this.state.suggestionsVisible !== oldState.suggestionsVisible)
|
197
|
+
) {
|
198
|
+
this.fetchExistingTags(this.state.value);
|
199
|
+
}
|
188
200
|
}
|
189
201
|
|
190
202
|
public handleLeaveMobileClick() {
|
@@ -447,7 +459,6 @@ export class TagsRaw<
|
|
447
459
|
if (!this.state.suggestionsVisible) {
|
448
460
|
this.props.onSuggestionsOpened?.();
|
449
461
|
}
|
450
|
-
this.fetchExistingTags(this.state.value);
|
451
462
|
this.setState(
|
452
463
|
() => ({ textIsFocused: true, suggestionsVisible: true, touched: true }),
|
453
464
|
() => this.handleErrors()
|
@@ -490,13 +501,11 @@ export class TagsRaw<
|
|
490
501
|
this.props.tags.concat(suggestions[this.state.preselectedSuggestion])
|
491
502
|
);
|
492
503
|
this.setState({ value: '', preselectedSuggestion: undefined }, () => {
|
493
|
-
this.fetchExistingTags();
|
494
504
|
this.handleErrors();
|
495
505
|
});
|
496
506
|
} else if (existingTag) {
|
497
507
|
this.props.onTagsChanged(this.props.tags.concat(existingTag));
|
498
508
|
this.setState({ value: '' }, () => {
|
499
|
-
this.fetchExistingTags();
|
500
509
|
this.handleErrors();
|
501
510
|
});
|
502
511
|
} else if (this.props.allowNew) {
|
@@ -534,13 +543,15 @@ export class TagsRaw<
|
|
534
543
|
? false
|
535
544
|
: this.state.suggestionsVisible,
|
536
545
|
});
|
546
|
+
const newTags = newTag
|
547
|
+
? this.props.tags
|
548
|
+
? this.props.tags.concat(newTag)
|
549
|
+
: [newTag]
|
550
|
+
: this.props.tags;
|
537
551
|
if (newTag) {
|
538
|
-
this.props.onTagsChanged(
|
539
|
-
this.props.tags ? this.props.tags.concat(newTag) : [newTag]
|
540
|
-
);
|
552
|
+
this.props.onTagsChanged(newTags);
|
541
553
|
}
|
542
554
|
this.setState({ value: '', textErrors: [] }, () => {
|
543
|
-
this.fetchExistingTags();
|
544
555
|
this.handleErrors();
|
545
556
|
});
|
546
557
|
};
|
@@ -599,7 +610,6 @@ export class TagsRaw<
|
|
599
610
|
}),
|
600
611
|
() => this.handleErrors()
|
601
612
|
);
|
602
|
-
this.fetchExistingTags(e.target.value);
|
603
613
|
};
|
604
614
|
|
605
615
|
private handleErrors = (tags: T[] = this.props.tags) => {
|
@@ -653,9 +663,17 @@ export class TagsRaw<
|
|
653
663
|
this.props
|
654
664
|
.fetchExistingTags(startsWith, this.props.tags)
|
655
665
|
.then((fetchedExistingTags) => {
|
666
|
+
if (fetchedExistingTags) {
|
667
|
+
clearTimeout(timer);
|
668
|
+
this.setState(() => ({
|
669
|
+
fetchedExistingTags,
|
670
|
+
fetchingExistingTags: false,
|
671
|
+
}));
|
672
|
+
}
|
673
|
+
})
|
674
|
+
.catch(() => {
|
656
675
|
clearTimeout(timer);
|
657
676
|
this.setState(() => ({
|
658
|
-
fetchedExistingTags,
|
659
677
|
fetchingExistingTags: false,
|
660
678
|
}));
|
661
679
|
});
|
@@ -699,11 +717,11 @@ export class TagsRaw<
|
|
699
717
|
circular={true}
|
700
718
|
blank={true}
|
701
719
|
onClick={this.tagRemoveClick(tag)}
|
702
|
-
className="
|
720
|
+
className="tags-input__tag__removeButton p-0"
|
703
721
|
Component={TagButtonComponent}
|
704
722
|
preventsDefault={false}
|
705
723
|
>
|
706
|
-
<PlusIcon />
|
724
|
+
<PlusIcon className="transform-rotate--45" />
|
707
725
|
</Button>
|
708
726
|
)}
|
709
727
|
</>
|
@@ -724,9 +742,9 @@ export class TagsRaw<
|
|
724
742
|
);
|
725
743
|
}
|
726
744
|
return (
|
727
|
-
<
|
745
|
+
<span onClick={this.tagClick(tag)} className={className} key={index}>
|
728
746
|
{body}
|
729
|
-
</
|
747
|
+
</span>
|
730
748
|
);
|
731
749
|
}
|
732
750
|
|
@@ -12,26 +12,27 @@
|
|
12
12
|
}
|
13
13
|
.tags-input__tag {
|
14
14
|
position: relative;
|
15
|
-
display: inline-flex;
|
16
|
-
align-items: center;
|
17
|
-
padding-left: 0.25rem;
|
18
15
|
margin-right: 0.25rem;
|
19
|
-
margin-bottom: 0.125rem;
|
20
|
-
margin-top: 0.125rem;
|
21
16
|
&.tags-input__tag-chip {
|
22
17
|
border: 1px solid;
|
23
18
|
border-radius: 1.5rem;
|
24
19
|
}
|
25
20
|
}
|
21
|
+
.tags-input__tag__removeButton {
|
22
|
+
margin-left: 2px;
|
23
|
+
svg {
|
24
|
+
vertical-align: -0.4rem;
|
25
|
+
}
|
26
|
+
}
|
26
27
|
.tags-input__tags__wrapper {
|
27
28
|
display: flex;
|
28
29
|
align-items: center;
|
30
|
+
line-height: 1.75;
|
29
31
|
}
|
30
32
|
.tags-input__tag__wrapper {
|
31
|
-
display: inline-flex;
|
32
|
-
flex-wrap: wrap;
|
33
33
|
padding-top: 0.25rem;
|
34
|
-
padding-
|
34
|
+
padding-left: 0.25rem;
|
35
|
+
padding-right: 0.25rem;
|
35
36
|
}
|
36
37
|
&.tags-input--readOnly {
|
37
38
|
border-bottom-color: transparent !important;
|
package/src/stories/TagsDemo.tsx
CHANGED
@@ -38,15 +38,19 @@ export function TagsDemo(props: TagsProps) {
|
|
38
38
|
<Tags
|
39
39
|
title="Tags suggestions"
|
40
40
|
label="Choose or create tags"
|
41
|
-
fetchExistingTags={(startsWith) =>
|
42
|
-
new Promise<Tag[]>((resolve) =>
|
41
|
+
fetchExistingTags={(startsWith, tags) => {
|
42
|
+
return new Promise<Tag[]>((resolve) =>
|
43
43
|
setTimeout(
|
44
44
|
() =>
|
45
|
-
resolve(
|
45
|
+
resolve(
|
46
|
+
existingTags
|
47
|
+
.filter((e) => e.name.includes(startsWith))
|
48
|
+
.filter((e) => !tags.find((t) => t.id === e.id))
|
49
|
+
),
|
46
50
|
1000
|
47
51
|
)
|
48
|
-
)
|
49
|
-
}
|
52
|
+
);
|
53
|
+
}}
|
50
54
|
suggestionsEmptyComponent={'Not found'}
|
51
55
|
tags={tags}
|
52
56
|
onTagsChanged={setTags}
|