fix(ui): tooltip positioning issues (#6439)

This commit is contained in:
Alessio Gravili
2024-05-20 16:37:53 -04:00
committed by GitHub
parent e682cb1b04
commit ed4766188d
31 changed files with 409 additions and 411 deletions

View File

@@ -84,14 +84,14 @@ const _RichText: React.FC<
width,
}}
>
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div className={`${baseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider
editorConfig={editorConfig}

View File

@@ -106,10 +106,6 @@
line-height: unset;
}
&__error-wrap {
position: relative;
}
&__rows {
display: flex;
flex-direction: column;

View File

@@ -1,8 +1,6 @@
@import '../scss/styles.scss';
.rich-text-lexical {
display: flex;
.errorBoundary {
pre {
text-wrap: unset;

View File

@@ -315,14 +315,14 @@ const RichTextField: React.FC<
width,
}}
>
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div className={`${baseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<Slate
editor={editor}
key={JSON.stringify({ initialValue, path })} // makes sure slate is completely re-rendered when initialValue changes, bypassing the slate-internal value memoization. That way, external changes to the form will update the editor

View File

@@ -6,7 +6,7 @@
opacity: 0;
background-color: var(--theme-elevation-800);
position: absolute;
z-index: 2;
z-index: 3;
left: 50%;
padding: base(0.2) base(0.4);
color: var(--theme-elevation-0);
@@ -52,8 +52,9 @@
}
}
&--position-top {
bottom: 100%;
top: calc(var(--base) * -0.6 - 13px);
transform: translate3d(-50%, calc(var(--caret-size) * -1), 0);
&::after {
@@ -62,8 +63,9 @@
}
}
&--position-bottom {
top: 100%;
bottom: calc(var(--base) * -0.6 - 13px);
transform: translate3d(-50%, var(--caret-size), 0);
&::after {

View File

@@ -11,6 +11,10 @@ export type Props = {
className?: string
delay?: number
show?: boolean
/**
* If the tooltip position should not change depending on if the toolbar is outside the boundingRef. @default false
*/
staticPositioning?: boolean
}
export const Tooltip: React.FC<Props> = (props) => {
@@ -21,6 +25,7 @@ export const Tooltip: React.FC<Props> = (props) => {
className,
delay = 350,
show: showFromProps = true,
staticPositioning = false,
} = props
const [show, setShow] = React.useState(showFromProps)
@@ -28,11 +33,14 @@ export const Tooltip: React.FC<Props> = (props) => {
const getTitleAttribute = (content) => (typeof content === 'string' ? content : '')
const [ref, intersectionEntry] = useIntersect({
root: boundingRef?.current || null,
rootMargin: '-145px 0px 0px 100px',
threshold: 0,
})
const [ref, intersectionEntry] = useIntersect(
{
root: boundingRef?.current || null,
rootMargin: '-145px 0px 0px 100px',
threshold: 0,
},
staticPositioning,
)
useEffect(() => {
let timerId: NodeJS.Timeout
@@ -52,22 +60,25 @@ export const Tooltip: React.FC<Props> = (props) => {
}, [showFromProps, delay])
useEffect(() => {
if (staticPositioning) return
setPosition(intersectionEntry?.isIntersecting ? 'top' : 'bottom')
}, [intersectionEntry])
}, [intersectionEntry, staticPositioning])
// The first aside is always on top. The purpose of that is that it can reliably be used for the interaction observer (as it's not moving around), to calculate the position of the actual tooltip.
return (
<React.Fragment>
<aside
aria-hidden="true"
className={['tooltip', className, `tooltip--caret-${alignCaret}`, 'tooltip--position-top']
.filter(Boolean)
.join(' ')}
ref={ref}
title={getTitleAttribute(children)}
>
<div className="tooltip-content">{children}</div>
</aside>
{!staticPositioning && (
<aside
aria-hidden="true"
className={['tooltip', className, `tooltip--caret-${alignCaret}`, 'tooltip--position-top']
.filter(Boolean)
.join(' ')}
ref={ref}
style={{ opacity: '0' }}
>
<div className="tooltip-content">{children}</div>
</aside>
)}
<aside
className={[
'tooltip',

View File

@@ -54,10 +54,6 @@
}
}
&__error-wrap {
position: relative;
}
&__row-header {
display: flex;
align-items: center;

View File

@@ -202,11 +202,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
>
{showError && (
<div className={`${baseClass}__error-wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
</div>
)}
{showError && <FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />}
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header-content`}>

View File

@@ -75,10 +75,6 @@
line-height: unset;
}
&__error-wrap {
position: relative;
}
&__rows {
display: flex;
flex-direction: column;

View File

@@ -215,11 +215,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
>
{showError && (
<div className={`${baseClass}__error-wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
</div>
)}
{showError && <FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />}
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__heading-with-error`}>

View File

@@ -10,10 +10,6 @@
margin-bottom: 0.2em;
max-width: fit-content;
}
&__error-wrap {
position: relative;
}
}
.checkbox-input {

View File

@@ -97,9 +97,7 @@ const CheckboxField: React.FC<CheckboxFieldProps> = (props) => {
width,
}}
>
<div className={`${baseClass}__error-wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
</div>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<CheckboxInput
AfterInput={AfterInput}
BeforeInput={BeforeInput}

View File

@@ -87,14 +87,14 @@ const CodeField: React.FC<CodeFieldProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div>
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{BeforeInput}
<CodeEditor
defaultLanguage={prismToMonacoLanguageMap[language] || language}

View File

@@ -50,21 +50,23 @@ export const ConfirmPassword: React.FC<ConfirmPasswordFieldProps> = (props) => {
.filter(Boolean)
.join(' ')}
>
<FieldError path={path} />
<FieldLabel
htmlFor="field-confirm-password"
label={t('authentication:confirmPassword')}
required
/>
<input
autoComplete="off"
disabled={!!disabled}
id="field-confirm-password"
name="confirm-password"
onChange={setValue}
type="password"
value={(value as string) || ''}
/>
<div className={`${fieldBaseClass}__wrap`}>
<FieldError path={path} />
<input
autoComplete="off"
disabled={!!disabled}
id="field-confirm-password"
name="confirm-password"
onChange={setValue}
type="password"
value={(value as string) || ''}
/>
</div>
</div>
)
}

View File

@@ -1,11 +1,5 @@
@import '../../scss/styles.scss';
.date-time-field {
&__error-wrap {
position: relative;
}
}
html[data-theme='light'] {
.date-time-field {
&--has-error {

View File

@@ -90,16 +90,14 @@ const DateTimeField: React.FC<DateFieldProps> = (props) => {
width,
}}
>
<div className={`${baseClass}__error-wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
</div>
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
<div className={`${fieldBaseClass}__wrap`} id={`field-${path.replace(/\./g, '__')}`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{BeforeInput}
<DatePickerField
{...datePickerProps}

View File

@@ -77,14 +77,14 @@ const EmailField: React.FC<EmailFieldProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div>
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{BeforeInput}
<input
autoComplete={autoComplete}

View File

@@ -130,14 +130,15 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{BeforeInput}
<CodeEditor
defaultLanguage="json"

View File

@@ -161,62 +161,64 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
required={required}
{...(labelProps || {})}
/>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
filterOption={(_, rawInput) => {
// eslint-disable-next-line no-restricted-globals
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
return isNumber(rawInput) && !isOverHasMany
}}
isClearable
isCreatable
isMulti
isSortable
noOptionsMessage={() => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return null
}}
// numberOnly
onChange={handleHasManyChange}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender as Option[]}
/>
) : (
<div>
{BeforeInput}
<input
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
max={max}
min={min}
name={path}
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
e.target.blur()
filterOption={(_, rawInput) => {
// eslint-disable-next-line no-restricted-globals
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
return isNumber(rawInput) && !isOverHasMany
}}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={typeof value === 'number' ? value : ''}
isClearable
isCreatable
isMulti
isSortable
noOptionsMessage={() => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return null
}}
// numberOnly
onChange={handleHasManyChange}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender as Option[]}
/>
{AfterInput}
</div>
)}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
) : (
<div>
{BeforeInput}
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
max={max}
min={min}
name={path}
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={typeof value === 'number' ? value : ''}
/>
{AfterInput}
</div>
)}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
</div>
</div>
)
}

View File

@@ -67,22 +67,25 @@ const PasswordField: React.FC<PasswordFieldProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<input
autoComplete={autoComplete}
disabled={formProcessing || disabled}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
type="password"
value={(value as string) || ''}
/>
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<input
autoComplete={autoComplete}
disabled={formProcessing || disabled}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
type="password"
value={(value as string) || ''}
/>
</div>
</div>
)
}

View File

@@ -1,10 +1,6 @@
@import '../../scss/styles.scss';
.radio-group {
&__error-wrap {
position: relative;
}
&--layout-horizontal {
ul {
display: flex;

View File

@@ -99,57 +99,58 @@ const RadioGroupField: React.FC<RadioFieldProps> = (props) => {
width,
}}
>
<div className={`${baseClass}__error-wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
</div>
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
{options.map((option) => {
let optionValue = ''
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
if (optionIsObject(option)) {
optionValue = option.value
} else {
optionValue = option
}
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
{options.map((option) => {
let optionValue = ''
const isSelected = String(optionValue) === String(value)
if (optionIsObject(option)) {
optionValue = option.value
} else {
optionValue = option
}
const id = `field-${path}-${optionValue}${uuid ? `-${uuid}` : ''}`
const isSelected = String(optionValue) === String(value)
return (
<li key={`${path} - ${optionValue}`}>
<Radio
id={id}
isSelected={isSelected}
onChange={() => {
if (typeof onChangeFromProps === 'function') {
onChangeFromProps(optionValue)
}
const id = `field-${path}-${optionValue}${uuid ? `-${uuid}` : ''}`
if (!readOnly) {
setValue(optionValue)
}
}}
option={optionIsObject(option) ? option : { label: option, value: option }}
path={path}
readOnly={readOnly}
uuid={uuid}
/>
</li>
)
})}
</ul>
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
return (
<li key={`${path} - ${optionValue}`}>
<Radio
id={id}
isSelected={isSelected}
onChange={() => {
if (typeof onChangeFromProps === 'function') {
onChangeFromProps(optionValue)
}
if (!readOnly) {
setValue(optionValue)
}
}}
option={optionIsObject(option) ? option : { label: option, value: option }}
path={path}
readOnly={readOnly}
uuid={uuid}
/>
</li>
)
})}
</ul>
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
</div>
</div>
)
}

View File

@@ -471,116 +471,119 @@ const RelationshipField: React.FC<RelationshipFieldProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
{!errorLoading && (
<div className={`${baseClass}__wrap`}>
<ReactSelect
backspaceRemovesValue={!drawerIsOpen}
components={{
MultiValueLabel,
SingleValue,
}}
customProps={{
disableKeyDown: drawerIsOpen,
disableMouseDown: drawerIsOpen,
onSave,
setDrawerIsOpen,
}}
disabled={readOnly || formProcessing || drawerIsOpen}
filterOption={enableWordBoundarySearch ? filterOption : undefined}
isLoading={isLoading}
isMulti={hasMany}
isSortable={isSortable}
onChange={
!readOnly
? (selected) => {
if (selected === null) {
setValue(hasMany ? [] : null)
} else if (hasMany) {
setValue(
selected
? selected.map((option) => {
if (hasMultipleRelations) {
return {
relationTo: option.relationTo,
value: option.value,
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{!errorLoading && (
<div className={`${baseClass}__wrap`}>
<ReactSelect
backspaceRemovesValue={!drawerIsOpen}
components={{
MultiValueLabel,
SingleValue,
}}
customProps={{
disableKeyDown: drawerIsOpen,
disableMouseDown: drawerIsOpen,
onSave,
setDrawerIsOpen,
}}
disabled={readOnly || formProcessing || drawerIsOpen}
filterOption={enableWordBoundarySearch ? filterOption : undefined}
isLoading={isLoading}
isMulti={hasMany}
isSortable={isSortable}
onChange={
!readOnly
? (selected) => {
if (selected === null) {
setValue(hasMany ? [] : null)
} else if (hasMany) {
setValue(
selected
? selected.map((option) => {
if (hasMultipleRelations) {
return {
relationTo: option.relationTo,
value: option.value,
}
}
}
return option.value
})
: null,
)
} else if (hasMultipleRelations) {
setValue({
relationTo: selected.relationTo,
value: selected.value,
})
} else {
setValue(selected.value)
return option.value
})
: null,
)
} else if (hasMultipleRelations) {
setValue({
relationTo: selected.relationTo,
value: selected.value,
})
} else {
setValue(selected.value)
}
}
}
: undefined
}
onInputChange={(newSearch) => handleInputChange(newSearch, value)}
onMenuClose={() => {
menuIsOpen.current = false
}}
onMenuOpen={() => {
menuIsOpen.current = true
: undefined
}
onInputChange={(newSearch) => handleInputChange(newSearch, value)}
onMenuClose={() => {
menuIsOpen.current = false
}}
onMenuOpen={() => {
menuIsOpen.current = true
if (!hasLoadedFirstPageRef.current) {
setIsLoading(true)
if (!hasLoadedFirstPageRef.current) {
setIsLoading(true)
void getResults({
lastLoadedPage: {},
onSuccess: () => {
hasLoadedFirstPageRef.current = true
setIsLoading(false)
},
value: initialValue,
})
}
}}
onMenuScrollToBottom={() => {
void getResults({
lastLoadedPage: {},
onSuccess: () => {
hasLoadedFirstPageRef.current = true
setIsLoading(false)
},
lastFullyLoadedRelation,
lastLoadedPage,
search,
sort: false,
value: initialValue,
})
}
}}
onMenuScrollToBottom={() => {
void getResults({
lastFullyLoadedRelation,
lastLoadedPage,
search,
sort: false,
value: initialValue,
})
}}
options={options}
showError={showError}
value={valueToRender ?? null}
/>
{!readOnly && allowCreate && (
<AddNewRelation
{...{
dispatchOptions,
hasMany,
options,
path,
relationTo,
setValue,
value,
}}
options={options}
showError={showError}
value={valueToRender ?? null}
/>
)}
</div>
)}
{errorLoading && <div className={`${baseClass}__error-loading`}>{errorLoading}</div>}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
{!readOnly && allowCreate && (
<AddNewRelation
{...{
dispatchOptions,
hasMany,
options,
path,
relationTo,
setValue,
value,
}}
/>
)}
</div>
)}
{errorLoading && <div className={`${baseClass}__error-loading`}>{errorLoading}</div>}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
</div>
</div>
)
}

View File

@@ -149,14 +149,15 @@ const SelectField: React.FC<SelectFieldProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{BeforeInput}
<ReactSelect
disabled={readOnly}

View File

@@ -60,61 +60,64 @@ export const TextInput: React.FC<TextInputProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
// prevent adding additional options if maxRows is reached
filterOption={() =>
!maxRows ? true : !(Array.isArray(value) && maxRows && value.length >= maxRows)
}
isClearable
isCreatable
isMulti
isSortable
noOptionsMessage={() => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return null
}}
onChange={onChange}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender}
/>
) : (
<div>
{BeforeInput}
<input
data-rtl={rtl}
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
id={`field-${path?.replace(/\./g, '__')}`}
name={path}
// prevent adding additional options if maxRows is reached
filterOption={() =>
!maxRows ? true : !(Array.isArray(value) && maxRows && value.length >= maxRows)
}
isClearable
isCreatable
isMulti
isSortable
noOptionsMessage={() => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return null
}}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender}
/>
{AfterInput}
</div>
)}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
) : (
<div>
{BeforeInput}
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path?.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
{AfterInput}
</div>
)}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
</div>
</div>
)
}

View File

@@ -54,36 +54,38 @@ export const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
{BeforeInput}
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
<textarea
className="textarea-element"
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
placeholder={getTranslation(placeholder, i18n)}
rows={rows}
value={value || ''}
/>
</div>
</label>
{AfterInput}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
{BeforeInput}
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
<textarea
className="textarea-element"
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
placeholder={getTranslation(placeholder, i18n)}
rows={rows}
value={value || ''}
/>
</div>
</label>
{AfterInput}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
</div>
</div>
)
}

View File

@@ -133,56 +133,59 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
width,
}}
>
<FieldError CustomError={CustomError} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
{collection?.upload && (
<React.Fragment>
{fileDoc && !missingFile && (
<FileDetails
collectionSlug={relationTo}
doc={fileDoc}
handleRemove={
readOnly
? undefined
: () => {
onChange(null)
}
}
uploadConfig={collection.upload}
/>
)}
{(!fileDoc || missingFile) && (
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__buttons`}>
<DocumentDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:uploadNewLabel', {
label: getTranslation(collection.labels.singular, i18n),
})}
</Button>
</DocumentDrawerToggler>
<ListDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:chooseFromExisting')}
</Button>
</ListDrawerToggler>
<div className={`${fieldBaseClass}__wrap`}>
<FieldError CustomError={CustomError} {...(errorProps || {})} />
{collection?.upload && (
<React.Fragment>
{fileDoc && !missingFile && (
<FileDetails
collectionSlug={relationTo}
doc={fileDoc}
handleRemove={
readOnly
? undefined
: () => {
onChange(null)
}
}
uploadConfig={collection.upload}
/>
)}
{(!fileDoc || missingFile) && (
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__buttons`}>
<DocumentDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:uploadNewLabel', {
label: getTranslation(collection.labels.singular, i18n),
})}
</Button>
</DocumentDrawerToggler>
<ListDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:chooseFromExisting')}
</Button>
</ListDrawerToggler>
</div>
</div>
</div>
)}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
</React.Fragment>
)}
{!readOnly && <DocumentDrawer onSave={onSave} />}
{!readOnly && <ListDrawer onSelect={onSelect} />}
)}
{CustomDescription !== undefined ? (
CustomDescription
) : (
<FieldDescription {...(descriptionProps || {})} />
)}
</React.Fragment>
)}
{!readOnly && <DocumentDrawer onSave={onSave} />}
{!readOnly && <ListDrawer onSelect={onSelect} />}
</div>
</div>
)
}

View File

@@ -2,8 +2,6 @@
.field-error.tooltip {
font-family: var(--font-body);
top: 0;
bottom: auto;
left: auto;
max-width: 75%;
right: calc(var(--base) * 0.5);
@@ -12,5 +10,6 @@
&::after {
border-top-color: var(--theme-error-500);
border-bottom-color: var(--theme-error-500);
}
}

View File

@@ -30,9 +30,9 @@ const DefaultFieldError: React.FC<ErrorProps> = (props) => {
const message = messageFromProps || errorMessage
const showMessage = showErrorFromProps || (hasSubmitted && valid === false)
if (showMessage) {
if (showMessage && message?.length) {
return (
<Tooltip alignCaret={alignCaret} className={baseClass} delay={0}>
<Tooltip alignCaret={alignCaret} className={baseClass} delay={0} staticPositioning>
{message}
</Tooltip>
)

View File

@@ -11,8 +11,13 @@
--spacing-field: 0;
}
.field-type__wrap {
position: relative;
}
& > .field-type {
margin-bottom: var(--spacing-field);
position: relative;
&[type='hidden'] {
margin-bottom: 0;

View File

@@ -12,7 +12,7 @@ export const useIntersect = (
const [node, setNode] = useState(null)
const observer = useRef(
typeof window !== 'undefined' && 'IntersectionObserver' in window
typeof window !== 'undefined' && 'IntersectionObserver' in window && !disable
? new window.IntersectionObserver(([ent]) => updateEntry(ent), {
root,
rootMargin,