Pull request 2679: AGDNS-4119 fix tooltip position on mobile
Some checks failed
build / test (macOS-latest) (push) Has been cancelled
build / test (ubuntu-latest) (push) Has been cancelled
build / test (windows-latest) (push) Has been cancelled
lint / go-lint (push) Has been cancelled
lint / eslint (push) Has been cancelled
build / build-release (push) Has been cancelled
build / notify (push) Has been cancelled
lint / notify (push) Has been cancelled

Fixed tooltips mobile position for pages
- DNS settings
- Encryption
- Rewrites modal

Squashed commit of the following:

commit 0993fa3b7e295594850892b8c2c3ddb111e3a016
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Jun 15 18:43:53 2026 +0300

    fix rewrites example text indent

commit e08691fd2614360b77742edd896e33bb32627e8b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Jun 15 18:36:52 2026 +0300

    fix imports

commit dbaee773f0b205a1ca76d6fc10a24771f6aba04f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Jun 15 18:35:21 2026 +0300

    AGDNS-4119 fix tooltip position on mobile
This commit is contained in:
Ildar Kamalov 2026-06-16 08:18:28 +00:00
parent 602f055f6b
commit 301cd4700d
13 changed files with 198 additions and 212 deletions

View file

@ -1,4 +1,5 @@
import React, { ReactNode } from 'react';
import React from 'react';
import type { ReactNode } from 'react';
import cn from 'clsx';
import { Dropdown } from 'panel/common/ui/Dropdown';
@ -15,6 +16,7 @@ type Props = {
menuClassName?: string;
overlayClassName?: string;
position?: 'bottomLeft' | 'bottomRight' | 'bottom';
label?: ReactNode;
};
export const FaqTooltip = ({
@ -24,11 +26,28 @@ export const FaqTooltip = ({
menuClassName,
overlayClassName,
position: positionProp,
label,
}: Props) => {
const isMobile = useIsMobile();
const currentPosition = isMobile ? 'bottom' : 'bottomLeft';
const position = positionProp ?? currentPosition;
const getDefaultPosition = () => {
if (isMobile) {
return label ? 'bottomLeft' : 'bottom';
}
return 'bottomLeft';
};
const position = positionProp ?? getDefaultPosition();
const trigger = label ? (
<span className={s.labelTrigger}>
{label}
<Icon icon="faq" className={s.icon} />
</span>
) : (
<Icon icon="faq" className={s.icon} />
);
return (
<Dropdown
@ -49,7 +68,7 @@ export const FaqTooltip = ({
noIcon
>
<div className={s.trigger} onPointerDown={(e) => e.stopPropagation()}>
<Icon icon="faq" className={s.icon} />
{trigger}
</div>
</Dropdown>
);

View file

@ -3,6 +3,13 @@
color: var(--default-gray-icons);
}
.labelTrigger {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.overlay_mobile {
@media (max-width: 767px) {
max-width: calc(100vw - 32px) !important;
@ -13,6 +20,7 @@
.menu {
padding: 16px;
background-color: var(--page-background-additional);
overflow-wrap: break-word;
@media (max-width: 767px) {
max-width: calc(100vw - 32px);

View file

@ -131,14 +131,12 @@ export const Form = ({ initialValues, onSubmit, processingSet }: FormProps) => {
id={id}
data-testid={id}
label={
<>
{title}
<FaqTooltip
text={faq}
menuSize="large"
spacing={id === 'blocked_hosts'}
/>
</>
<FaqTooltip
text={faq}
menuSize="large"
spacing={id === 'blocked_hosts'}
label={title}
/>
}
errorMessage={fieldState.error?.message}
onBlur={(e) => {

View file

@ -187,12 +187,7 @@ export const Form = ({ initialValues, onSubmit }: CacheFormProps) => {
{...field}
type="number"
id={name}
label={
<>
{title}
<FaqTooltip text={faq} menuSize="large" />
</>
}
label={<FaqTooltip text={faq} menuSize="large" label={title} />}
placeholder={placeholder}
min={0}
max={UINT32_RANGE.MAX}

View file

@ -144,13 +144,11 @@ export const Form = ({ processing, initialValues, onSubmit }: Props) => {
data-testid="dns_config_ratelimit"
type="number"
label={
<>
{intl.getMessage('server_config_rate_limit')}
<FaqTooltip
text={intl.getMessage('server_config_rate_limit_faq')}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('server_config_rate_limit_faq')}
menuSize="large"
label={intl.getMessage('server_config_rate_limit')}
/>
}
placeholder={intl.getMessage(
'server_config_rate_limit_placeholder',
@ -178,15 +176,11 @@ export const Form = ({ processing, initialValues, onSubmit }: Props) => {
data-testid="dns_config_subnet_ipv4"
type="number"
label={
<>
{intl.getMessage('server_config_subnet_len_ipv4')}
<FaqTooltip
text={intl.getMessage(
'server_config_subnet_len_ipv4_faq',
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('server_config_subnet_len_ipv4_faq')}
menuSize="large"
label={intl.getMessage('server_config_subnet_len_ipv4')}
/>
}
placeholder={intl.getMessage(
'server_config_subnet_len_placeholder',
@ -214,15 +208,11 @@ export const Form = ({ processing, initialValues, onSubmit }: Props) => {
data-testid="dns_config_subnet_ipv6"
type="number"
label={
<>
{intl.getMessage('server_config_subnet_len_ipv6')}
<FaqTooltip
text={intl.getMessage(
'server_config_subnet_len_ipv6_faq',
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('server_config_subnet_len_ipv6_faq')}
menuSize="large"
label={intl.getMessage('server_config_subnet_len_ipv6')}
/>
}
placeholder={intl.getMessage(
'server_config_subnet_len_placeholder',
@ -249,15 +239,15 @@ export const Form = ({ processing, initialValues, onSubmit }: Props) => {
{...field}
data-testid="dns_config_ratelimit_whitelist"
label={
<>
{intl.getMessage('server_config_rate_limit_whitelist')}
<FaqTooltip
text={intl.getMessage(
'server_config_rate_limit_whitelist_faq',
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage(
'server_config_rate_limit_whitelist_faq',
)}
menuSize="large"
label={intl.getMessage(
'server_config_rate_limit_whitelist',
)}
/>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
errorMessage={fieldState.error?.message}
@ -419,10 +409,11 @@ export const Form = ({ processing, initialValues, onSubmit }: Props) => {
data-testid={`dns_config_${name}`}
type="text"
label={
<>
{label}
<FaqTooltip text={faq} menuSize="large" />
</>
<FaqTooltip
text={faq}
menuSize="large"
label={label}
/>
}
placeholder={placeholder}
errorMessage={fieldState.error?.message}
@ -445,15 +436,13 @@ export const Form = ({ processing, initialValues, onSubmit }: Props) => {
data-testid="dns_config_blocked_response_ttl"
type="number"
label={
<>
{intl.getMessage('server_config_blocking_mode_ttl')}
<FaqTooltip
text={intl.getMessage(
'server_config_blocking_mode_ttl_faq',
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage(
'server_config_blocking_mode_ttl_faq',
)}
menuSize="large"
label={intl.getMessage('server_config_blocking_mode_ttl')}
/>
}
placeholder={intl.getMessage('form_enter_blocked_response_ttl')}
errorMessage={fieldState.error?.message}

View file

@ -128,37 +128,32 @@ export const Form = ({ initialValues, onSubmit }: FormProps) => {
{...field}
id="upstream_dns"
label={
<>
{intl.getMessage('upstream_dns_addresses')}
<FaqTooltip
text={intl.getMessage(
'upstream_dns_addresses_faq',
{
a: (text: string) => (
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}
>
{text}
</a>
),
b: (text: string) => (
<a
href="https://link.adtidy.org/forward.html?action=dns_kb_providers&from=ui&app=home"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}
>
{text}
</a>
),
},
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('upstream_dns_addresses_faq', {
a: (text: string) => (
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}
>
{text}
</a>
),
b: (text: string) => (
<a
href="https://link.adtidy.org/forward.html?action=dns_kb_providers&from=ui&app=home"
target="_blank"
rel="noopener noreferrer"
className={theme.link.link}
>
{text}
</a>
),
})}
menuSize="large"
label={intl.getMessage('upstream_dns_addresses')}
/>
}
placeholder={intl.getMessage('upstream_dns_placeholder')}
disabled={!!upstreamDnsFile || processingTestUpstream}
@ -201,13 +196,11 @@ export const Form = ({ initialValues, onSubmit }: FormProps) => {
{...field}
id="fallback_dns"
label={
<>
{intl.getMessage('upstream_fallback_title')}
<FaqTooltip
text={intl.getMessage('upstream_fallback_title_faq')}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('upstream_fallback_title_faq')}
menuSize="large"
label={intl.getMessage('upstream_fallback_title')}
/>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
size="medium"
@ -228,13 +221,11 @@ export const Form = ({ initialValues, onSubmit }: FormProps) => {
id="bootstrap_dns"
data-testid="bootstrap_dns"
label={
<>
{intl.getMessage('upstream_bootstrap_dns_title')}
<FaqTooltip
text={intl.getMessage('upstream_bootstrap_dns_faq')}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('upstream_bootstrap_dns_faq')}
menuSize="large"
label={intl.getMessage('upstream_bootstrap_dns_title')}
/>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
size="medium"
@ -255,35 +246,29 @@ export const Form = ({ initialValues, onSubmit }: FormProps) => {
id="local_ptr_upstreams"
data-testid="local_ptr_upstreams"
label={
<>
{intl.getMessage('upstream_ptr')}
<FaqTooltip
text={
<>
<FaqTooltip
text={
<>
<div>
{intl.getMessage('upstream_ptr_faq_1', {
value: '192.168.1.1/24',
})}
</div>
<div>{intl.getMessage('upstream_ptr_faq_2')}</div>
{defaultLocalPtrUpstreams?.length > 0 && (
<div>
{intl.getMessage('upstream_ptr_faq_1', {
value: '192.168.1.1/24',
{intl.getMessage('upstream_ptr_faq_3', {
value_1: defaultLocalPtrUpstreams[0],
value_2: defaultLocalPtrUpstreams[1],
})}
</div>
<div>
{intl.getMessage('upstream_ptr_faq_2')}
</div>
{defaultLocalPtrUpstreams?.length > 0 && (
<div>
{intl.getMessage('upstream_ptr_faq_3', {
value_1:
defaultLocalPtrUpstreams[0],
value_2:
defaultLocalPtrUpstreams[1],
})}
</div>
)}
</>
}
menuSize="large"
spacing
/>
</>
)}
</>
}
menuSize="large"
spacing
label={intl.getMessage('upstream_ptr')}
/>
}
placeholder={intl.getMessage('ip_addresses_placeholder')}
size="medium"
@ -355,13 +340,11 @@ export const Form = ({ initialValues, onSubmit }: FormProps) => {
type="number"
id="upstream_timeout"
label={
<>
{intl.getMessage('upstream_timeout')}
<FaqTooltip
text={intl.getMessage('upstream_timeout_faq')}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('upstream_timeout_faq')}
menuSize="large"
label={intl.getMessage('upstream_timeout')}
/>
}
placeholder={intl.getMessage('upstream_timeout_placeholder')}
min={1}

View file

@ -274,27 +274,24 @@ export const Form = ({ initialValues, encryption, onSubmit, debouncedConfigValid
{...field}
type="text"
label={
<>
{intl.getMessage('encryption_server')}
<FaqTooltip
text={
<>
<div className={s.tooltipText}>
{intl.getMessage(
'encryption_server_tooltip_1',
)}
</div>
<div className={s.tooltipText}>
{intl.getMessage(
'encryption_server_tooltip_2',
)}
</div>
</>
}
menuSize="large"
/>
</>
<FaqTooltip
text={
<>
<div className={s.tooltipText}>
{intl.getMessage(
'encryption_server_tooltip_1',
)}
</div>
<div className={s.tooltipText}>
{intl.getMessage(
'encryption_server_tooltip_2',
)}
</div>
</>
}
menuSize="large"
label={intl.getMessage('encryption_server')}
/>
}
placeholder={intl.getMessage('encryption_server_enter')}
errorMessage={fieldState.error?.message}
@ -317,16 +314,13 @@ export const Form = ({ initialValues, encryption, onSubmit, debouncedConfigValid
{...field}
type="number"
label={
<>
{intl.getMessage('encryption_https')}
<FaqTooltip
text={intl.getMessage(
'encryption_https_tooltip',
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage(
'encryption_https_tooltip',
)}
menuSize="large"
label={intl.getMessage('encryption_https')}
/>
}
placeholder={intl.getMessage('encryption_https')}
errorMessage={fieldState.error?.message}
@ -353,16 +347,11 @@ export const Form = ({ initialValues, encryption, onSubmit, debouncedConfigValid
{...field}
type="number"
label={
<>
{intl.getMessage('encryption_dot')}
<FaqTooltip
text={intl.getMessage(
'encryption_dot_tooltip',
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('encryption_dot_tooltip')}
menuSize="large"
label={intl.getMessage('encryption_dot')}
/>
}
placeholder={intl.getMessage('encryption_dot')}
errorMessage={fieldState.error?.message}
@ -389,16 +378,11 @@ export const Form = ({ initialValues, encryption, onSubmit, debouncedConfigValid
{...field}
type="number"
label={
<>
{intl.getMessage('encryption_doq')}
<FaqTooltip
text={intl.getMessage(
'encryption_doq_tooltip',
)}
menuSize="large"
/>
</>
<FaqTooltip
text={intl.getMessage('encryption_doq_tooltip')}
menuSize="large"
label={intl.getMessage('encryption_doq')}
/>
}
placeholder={intl.getMessage('encryption_doq')}
errorMessage={fieldState.error?.message}

View file

@ -1,4 +1,5 @@
import React from 'react';
import type { ReactNode } from 'react';
import cn from 'clsx';
import intl from 'panel/common/intl';
@ -24,11 +25,16 @@ const items = [
},
];
export const AnswerFaqTooltip = () => {
type Props = {
label?: ReactNode;
};
export const AnswerFaqTooltip = ({ label }: Props) => {
return (
<FaqTooltip
overlayClassName={s.dropdown}
menuClassName={s.tooltip}
label={label}
text={
<>
<div className={cn(theme.text.t3, s.tooltipTitle)}>

View file

@ -17,10 +17,11 @@
}
.tooltipItem {
padding: 8px 0;
position: relative;
padding: 8px 0 8px 28px;
&:last-child {
padding: 8px 0 0;
padding: 8px 0 0 28px;
}
& code {
@ -37,6 +38,8 @@
}
.icon {
margin-right: 8px;
position: absolute;
top: 9px;
left: 0;
color: var(--default-black-icons);
}

View file

@ -151,11 +151,9 @@ export const ConfigureRewritesModal = ({ modalId, rewriteToEdit, onSubmit, onClo
id="domain"
data-testid="rewrite-domain-input"
label={
<>
{intl.getMessage('rewrite_domain')}
<DomainFaqTooltip />
</>
<DomainFaqTooltip
label={intl.getMessage('rewrite_domain')}
/>
}
placeholder={intl.getMessage(
'rewrite_domain_input_placeholder',
@ -180,11 +178,9 @@ export const ConfigureRewritesModal = ({ modalId, rewriteToEdit, onSubmit, onClo
id="answer"
data-testid="rewrite-answer-input"
label={
<>
{intl.getMessage('result')}
<AnswerFaqTooltip />
</>
<AnswerFaqTooltip
label={intl.getMessage('result')}
/>
}
placeholder={intl.getMessage(
'rewrites_answer_input_placeholder',

View file

@ -1,4 +1,5 @@
import React from 'react';
import type { ReactNode } from 'react';
import cn from 'clsx';
import intl from 'panel/common/intl';
@ -18,11 +19,16 @@ const items = [
},
];
export const DomainFaqTooltip = () => {
type Props = {
label?: ReactNode;
};
export const DomainFaqTooltip = ({ label }: Props) => {
return (
<FaqTooltip
overlayClassName={s.dropdown}
menuClassName={s.tooltip}
label={label}
text={
<>
<div className={cn(theme.text.t3, s.tooltipTitle)}>

View file

@ -48,8 +48,6 @@ export const IgnoredDomains = ({
id={textareaId}
label={
<>
{intl.getMessage('settings_domain_names')}
<FaqTooltip
text={
<>
@ -66,6 +64,7 @@ export const IgnoredDomains = ({
</div>
</>
}
label={intl.getMessage('settings_domain_names')}
/>
<a

View file

@ -147,7 +147,6 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
<div className={cn(s.formGroup, s.formGroupSettings)}>
<label htmlFor="clientId" className={cn(s.formLabel, s.formLabelWithDesc)}>
{intl.getMessage('client_id')}
<FaqTooltip
text={intl.getMessage('client_id_faq', {
a: (text: string) => (
@ -161,6 +160,7 @@ export const MobileConfigForm = ({ initialValues }: Props) => {
</a>
),
})}
label={intl.getMessage('client_id')}
/>
</label>