fix(ui): tooltip positioning issues (#6439)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -106,10 +106,6 @@
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
&__error-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
@import '../scss/styles.scss';
|
||||
|
||||
.rich-text-lexical {
|
||||
display: flex;
|
||||
|
||||
.errorBoundary {
|
||||
pre {
|
||||
text-wrap: unset;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -54,10 +54,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__error-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -75,10 +75,6 @@
|
||||
line-height: unset;
|
||||
}
|
||||
|
||||
&__error-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -10,10 +10,6 @@
|
||||
margin-bottom: 0.2em;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
&__error-wrap {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-input {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
.date-time-field {
|
||||
&__error-wrap {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='light'] {
|
||||
.date-time-field {
|
||||
&--has-error {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
.radio-group {
|
||||
&__error-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--layout-horizontal {
|
||||
ul {
|
||||
display: flex;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user