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, width,
}} }}
> >
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} /> <FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}> <ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider <LexicalProvider
editorConfig={editorConfig} editorConfig={editorConfig}

View File

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

View File

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

View File

@@ -315,14 +315,14 @@ const RichTextField: React.FC<
width, width,
}} }}
> >
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} /> <FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
<FieldLabel
CustomLabel={CustomLabel}
label={label}
required={required}
{...(labelProps || {})}
/>
<Slate <Slate
editor={editor} 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 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; opacity: 0;
background-color: var(--theme-elevation-800); background-color: var(--theme-elevation-800);
position: absolute; position: absolute;
z-index: 2; z-index: 3;
left: 50%; left: 50%;
padding: base(0.2) base(0.4); padding: base(0.2) base(0.4);
color: var(--theme-elevation-0); color: var(--theme-elevation-0);
@@ -52,8 +52,9 @@
} }
} }
&--position-top { &--position-top {
bottom: 100%; top: calc(var(--base) * -0.6 - 13px);
transform: translate3d(-50%, calc(var(--caret-size) * -1), 0); transform: translate3d(-50%, calc(var(--caret-size) * -1), 0);
&::after { &::after {
@@ -62,8 +63,9 @@
} }
} }
&--position-bottom { &--position-bottom {
top: 100%; bottom: calc(var(--base) * -0.6 - 13px);
transform: translate3d(-50%, var(--caret-size), 0); transform: translate3d(-50%, var(--caret-size), 0);
&::after { &::after {

View File

@@ -11,6 +11,10 @@ export type Props = {
className?: string className?: string
delay?: number delay?: number
show?: boolean 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) => { export const Tooltip: React.FC<Props> = (props) => {
@@ -21,6 +25,7 @@ export const Tooltip: React.FC<Props> = (props) => {
className, className,
delay = 350, delay = 350,
show: showFromProps = true, show: showFromProps = true,
staticPositioning = false,
} = props } = props
const [show, setShow] = React.useState(showFromProps) 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 getTitleAttribute = (content) => (typeof content === 'string' ? content : '')
const [ref, intersectionEntry] = useIntersect({ const [ref, intersectionEntry] = useIntersect(
root: boundingRef?.current || null, {
rootMargin: '-145px 0px 0px 100px', root: boundingRef?.current || null,
threshold: 0, rootMargin: '-145px 0px 0px 100px',
}) threshold: 0,
},
staticPositioning,
)
useEffect(() => { useEffect(() => {
let timerId: NodeJS.Timeout let timerId: NodeJS.Timeout
@@ -52,22 +60,25 @@ export const Tooltip: React.FC<Props> = (props) => {
}, [showFromProps, delay]) }, [showFromProps, delay])
useEffect(() => { useEffect(() => {
if (staticPositioning) return
setPosition(intersectionEntry?.isIntersecting ? 'top' : 'bottom') 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 ( return (
<React.Fragment> <React.Fragment>
<aside {!staticPositioning && (
aria-hidden="true" <aside
className={['tooltip', className, `tooltip--caret-${alignCaret}`, 'tooltip--position-top'] aria-hidden="true"
.filter(Boolean) className={['tooltip', className, `tooltip--caret-${alignCaret}`, 'tooltip--position-top']
.join(' ')} .filter(Boolean)
ref={ref} .join(' ')}
title={getTitleAttribute(children)} ref={ref}
> style={{ opacity: '0' }}
<div className="tooltip-content">{children}</div> >
</aside> <div className="tooltip-content">{children}</div>
</aside>
)}
<aside <aside
className={[ className={[
'tooltip', 'tooltip',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,16 +90,14 @@ const DateTimeField: React.FC<DateFieldProps> = (props) => {
width, width,
}} }}
> >
<div className={`${baseClass}__error-wrap`}>
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
</div>
<FieldLabel <FieldLabel
CustomLabel={CustomLabel} CustomLabel={CustomLabel}
label={label} label={label}
required={required} required={required}
{...(labelProps || {})} {...(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} {BeforeInput}
<DatePickerField <DatePickerField
{...datePickerProps} {...datePickerProps}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,6 @@
.field-error.tooltip { .field-error.tooltip {
font-family: var(--font-body); font-family: var(--font-body);
top: 0;
bottom: auto;
left: auto; left: auto;
max-width: 75%; max-width: 75%;
right: calc(var(--base) * 0.5); right: calc(var(--base) * 0.5);
@@ -12,5 +10,6 @@
&::after { &::after {
border-top-color: var(--theme-error-500); 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 message = messageFromProps || errorMessage
const showMessage = showErrorFromProps || (hasSubmitted && valid === false) const showMessage = showErrorFromProps || (hasSubmitted && valid === false)
if (showMessage) { if (showMessage && message?.length) {
return ( return (
<Tooltip alignCaret={alignCaret} className={baseClass} delay={0}> <Tooltip alignCaret={alignCaret} className={baseClass} delay={0} staticPositioning>
{message} {message}
</Tooltip> </Tooltip>
) )

View File

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

View File

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