fix(tradein/v2): a11y P3b — клавиатурная форма 01 (#2062) #2078
1 changed files with 288 additions and 26 deletions
|
|
@ -11,7 +11,16 @@
|
||||||
// the CRM dropdown still has no backend → kept visually but disabled. Hover/active
|
// the CRM dropdown still has no backend → kept visually but disabled. Hover/active
|
||||||
// + @keyframes live in a pp-prefixed local <style>.
|
// + @keyframes live in a pp-prefixed local <style>.
|
||||||
|
|
||||||
import { useRef, useState, type CSSProperties } from "react";
|
import {
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type CSSProperties,
|
||||||
|
type Dispatch,
|
||||||
|
type KeyboardEvent as ReactKeyboardEvent,
|
||||||
|
type SetStateAction,
|
||||||
|
} from "react";
|
||||||
import { tokens } from "./tokens";
|
import { tokens } from "./tokens";
|
||||||
import { dropdownOptions } from "./fixtures";
|
import { dropdownOptions } from "./fixtures";
|
||||||
import {
|
import {
|
||||||
|
|
@ -47,6 +56,69 @@ interface DdProps {
|
||||||
triggerColor?: string;
|
triggerColor?: string;
|
||||||
caretColor?: string;
|
caretColor?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared combobox keydown logic for the HUD dropdowns (the <Dd> widget + the
|
||||||
|
// inline РАДИУС copy). aria-activedescendant pattern: focus STAYS on the trigger
|
||||||
|
// button; arrows move a virtual `activeIdx` highlight, Enter/Space commit it,
|
||||||
|
// Escape/Tab close. Navigation keys preventDefault to stop page scroll.
|
||||||
|
function comboKeyDown(
|
||||||
|
e: ReactKeyboardEvent<HTMLButtonElement>,
|
||||||
|
open: boolean,
|
||||||
|
options: string[],
|
||||||
|
activeIdx: number,
|
||||||
|
setActiveIdx: Dispatch<SetStateAction<number>>,
|
||||||
|
onToggle: () => void,
|
||||||
|
onSelect: (v: string) => void,
|
||||||
|
) {
|
||||||
|
if (!open) {
|
||||||
|
if (
|
||||||
|
e.key === "ArrowDown" ||
|
||||||
|
e.key === "ArrowUp" ||
|
||||||
|
e.key === "Enter" ||
|
||||||
|
e.key === " "
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
onToggle();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveIdx((i) => Math.min(options.length - 1, i + 1));
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveIdx((i) => Math.max(0, i - 1));
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveIdx(0);
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
e.preventDefault();
|
||||||
|
setActiveIdx(options.length - 1);
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
case " ":
|
||||||
|
e.preventDefault();
|
||||||
|
if (activeIdx >= 0 && activeIdx < options.length) {
|
||||||
|
onSelect(options[activeIdx]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
e.preventDefault();
|
||||||
|
onToggle();
|
||||||
|
break;
|
||||||
|
case "Tab":
|
||||||
|
// do NOT preventDefault — let focus move naturally; just close the list
|
||||||
|
onToggle();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Dd({
|
function Dd({
|
||||||
|
|
@ -65,7 +137,23 @@ function Dd({
|
||||||
triggerColor = tokens.ink,
|
triggerColor = tokens.ink,
|
||||||
caretColor = tokens.muted2,
|
caretColor = tokens.muted2,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
ariaLabel,
|
||||||
}: DdProps) {
|
}: DdProps) {
|
||||||
|
const listId = useId();
|
||||||
|
const optId = (i: number) => `${listId}-opt-${i}`;
|
||||||
|
const [activeIdx, setActiveIdx] = useState(-1);
|
||||||
|
|
||||||
|
// On open, point the virtual highlight at the current selection (fallback to
|
||||||
|
// the first option); reset when closed. Pure UI sync — not a data effect.
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
const idx = options.indexOf(value);
|
||||||
|
setActiveIdx(idx >= 0 ? idx : 0);
|
||||||
|
} else {
|
||||||
|
setActiveIdx(-1);
|
||||||
|
}
|
||||||
|
}, [open, value, options]);
|
||||||
|
|
||||||
const panelStyle: CSSProperties = {
|
const panelStyle: CSSProperties = {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|
@ -88,7 +176,8 @@ function Dd({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className={
|
className={
|
||||||
disabled
|
disabled
|
||||||
? "pp-dd-trigger-disabled"
|
? "pp-dd-trigger-disabled"
|
||||||
|
|
@ -96,7 +185,31 @@ function Dd({
|
||||||
? "pp-dd-trigger-dashed"
|
? "pp-dd-trigger-dashed"
|
||||||
: "pp-dd-trigger"
|
: "pp-dd-trigger"
|
||||||
}
|
}
|
||||||
onClick={onToggle}
|
onClick={!disabled ? onToggle : undefined}
|
||||||
|
onKeyDown={
|
||||||
|
disabled
|
||||||
|
? undefined
|
||||||
|
: (e) =>
|
||||||
|
comboKeyDown(
|
||||||
|
e,
|
||||||
|
open,
|
||||||
|
options,
|
||||||
|
activeIdx,
|
||||||
|
setActiveIdx,
|
||||||
|
onToggle,
|
||||||
|
onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
role="combobox"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={listId}
|
||||||
|
aria-activedescendant={
|
||||||
|
open && activeIdx >= 0 ? optId(activeIdx) : undefined
|
||||||
|
}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-disabled={disabled || undefined}
|
||||||
|
disabled={disabled}
|
||||||
style={{
|
style={{
|
||||||
height: triggerHeight,
|
height: triggerHeight,
|
||||||
background: triggerBg,
|
background: triggerBg,
|
||||||
|
|
@ -109,6 +222,8 @@ function Dd({
|
||||||
fontSize: triggerFontSize,
|
fontSize: triggerFontSize,
|
||||||
color: triggerColor,
|
color: triggerColor,
|
||||||
cursor: disabled ? "default" : "pointer",
|
cursor: disabled ? "default" : "pointer",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
|
|
@ -123,14 +238,24 @@ function Dd({
|
||||||
▼
|
▼
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div style={panelStyle}>
|
<div
|
||||||
{options.map((o) => (
|
style={panelStyle}
|
||||||
|
role="listbox"
|
||||||
|
id={listId}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{options.map((o, i) => (
|
||||||
<div
|
<div
|
||||||
key={o}
|
key={o}
|
||||||
className="pp-dd-opt"
|
className="pp-dd-opt"
|
||||||
|
role="option"
|
||||||
|
id={optId(i)}
|
||||||
|
aria-selected={o === value}
|
||||||
onClick={() => onSelect(o)}
|
onClick={() => onSelect(o)}
|
||||||
|
onMouseEnter={() => setActiveIdx(i)}
|
||||||
style={{
|
style={{
|
||||||
padding: "7px 10px",
|
padding: "7px 10px",
|
||||||
fontSize: optionFontSize,
|
fontSize: optionFontSize,
|
||||||
|
|
@ -139,6 +264,7 @@ function Dd({
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: o === value ? tokens.accent : tokens.ink,
|
color: o === value ? tokens.accent : tokens.ink,
|
||||||
fontWeight: o === value ? 600 : 400,
|
fontWeight: o === value ? 600 : 400,
|
||||||
|
background: i === activeIdx ? "rgba(46,139,255,.1)" : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{o}
|
{o}
|
||||||
|
|
@ -353,6 +479,9 @@ export default function ParamsPanel({
|
||||||
markers = [],
|
markers = [],
|
||||||
}: ParamsPanelProps) {
|
}: ParamsPanelProps) {
|
||||||
const [openDd, setOpenDd] = useState<DdKey>(null);
|
const [openDd, setOpenDd] = useState<DdKey>(null);
|
||||||
|
// РАДИУС combobox a11y state (aria-activedescendant highlight + listbox id).
|
||||||
|
const radiusListId = useId();
|
||||||
|
const [radiusActive, setRadiusActive] = useState(-1);
|
||||||
// Map zoom — applied as transform: scale() on the map content layer (not the
|
// Map zoom — applied as transform: scale() on the map content layer (not the
|
||||||
// whole panel). Default 1 (identity → pixel-identical), step .25, clamp 1–2.5
|
// whole panel). Default 1 (identity → pixel-identical), step .25, clamp 1–2.5
|
||||||
// (no zoom-out below 1: there is no real basemap behind the blueprint SVG, so
|
// (no zoom-out below 1: there is no real basemap behind the blueprint SVG, so
|
||||||
|
|
@ -406,6 +535,9 @@ export default function ParamsPanel({
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
// БАЛКОН radiogroup focus targets (roving tabindex).
|
||||||
|
const balNoRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const balYesRef = useRef<HTMLButtonElement>(null);
|
||||||
const suggest = useGeocodeSuggest(addressQuery);
|
const suggest = useGeocodeSuggest(addressQuery);
|
||||||
|
|
||||||
const handleAddressChange = (v: string) => {
|
const handleAddressChange = (v: string) => {
|
||||||
|
|
@ -436,6 +568,34 @@ export default function ParamsPanel({
|
||||||
setOpenDd(null);
|
setOpenDd(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep the РАДИУС virtual highlight pointed at the open selection (UI sync,
|
||||||
|
// not a data effect) — mirrors the <Dd> internal effect.
|
||||||
|
useEffect(() => {
|
||||||
|
if (openDd === "radius") {
|
||||||
|
const idx = RADIUS_OPTIONS.indexOf(radius);
|
||||||
|
setRadiusActive(idx >= 0 ? idx : 0);
|
||||||
|
} else {
|
||||||
|
setRadiusActive(-1);
|
||||||
|
}
|
||||||
|
}, [openDd, radius]);
|
||||||
|
|
||||||
|
// БАЛКОН radiogroup keyboard: arrows pick + move focus; Space/Enter commit the
|
||||||
|
// focused option.
|
||||||
|
const handleBalconyKey = (e: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
setBalcony(true);
|
||||||
|
balYesRef.current?.focus();
|
||||||
|
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
setBalcony(false);
|
||||||
|
balNoRef.current?.focus();
|
||||||
|
} else if (e.key === " " || e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
setBalcony(e.currentTarget === balYesRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const trimmedAddress = address.trim();
|
const trimmedAddress = address.trim();
|
||||||
const areaNum = Number(area.replace(",", "."));
|
const areaNum = Number(area.replace(",", "."));
|
||||||
|
|
@ -493,8 +653,14 @@ export default function ParamsPanel({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
// No Tailwind preflight here → reset UA <button> chrome explicitly so the
|
||||||
|
// radio pills keep the HUD font/size (UA buttons don't inherit font-family).
|
||||||
|
// `inherit` picks up the artboard's Manrope, matching the prior <div> + the
|
||||||
|
// map +/- buttons (strict parity, one approach).
|
||||||
|
fontFamily: "inherit",
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
|
padding: 0,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "all .15s",
|
transition: "all .15s",
|
||||||
};
|
};
|
||||||
|
|
@ -701,7 +867,9 @@ export default function ParamsPanel({
|
||||||
gap: 5,
|
gap: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Приблизить карту"
|
||||||
style={{
|
style={{
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
|
|
@ -715,12 +883,17 @@ export default function ParamsPanel({
|
||||||
transition: "all .15s",
|
transition: "all .15s",
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: tokens.muted,
|
color: tokens.muted,
|
||||||
|
padding: 0,
|
||||||
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
onClick={() => setZoom((z) => Math.min(2.5, z + 0.25))}
|
onClick={() => setZoom((z) => Math.min(2.5, z + 0.25))}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</div>
|
</button>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Отдалить карту"
|
||||||
|
disabled={zoom <= 1}
|
||||||
style={{
|
style={{
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
|
|
@ -735,13 +908,15 @@ export default function ParamsPanel({
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: tokens.muted,
|
color: tokens.muted,
|
||||||
opacity: zoom <= 1 ? 0.4 : 1,
|
opacity: zoom <= 1 ? 0.4 : 1,
|
||||||
|
padding: 0,
|
||||||
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (zoom > 1) setZoom((z) => Math.max(1, z - 0.25));
|
if (zoom > 1) setZoom((z) => Math.max(1, z - 0.25));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
−
|
−
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* map corner bracket */}
|
{/* map corner bracket */}
|
||||||
|
|
@ -775,9 +950,31 @@ export default function ParamsPanel({
|
||||||
submit). Trigger markup unchanged; now toggles a Dd-style option panel
|
submit). Trigger markup unchanged; now toggles a Dd-style option panel
|
||||||
and uses the shared pp-dd-trigger hover cue. */}
|
and uses the shared pp-dd-trigger hover cue. */}
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
className="pp-dd-trigger"
|
className="pp-dd-trigger"
|
||||||
onClick={() => toggle("radius")}
|
onClick={() => toggle("radius")}
|
||||||
|
onKeyDown={(e) =>
|
||||||
|
comboKeyDown(
|
||||||
|
e,
|
||||||
|
openDd === "radius",
|
||||||
|
RADIUS_OPTIONS,
|
||||||
|
radiusActive,
|
||||||
|
setRadiusActive,
|
||||||
|
() => toggle("radius"),
|
||||||
|
pick(setRadius),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
role="combobox"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={openDd === "radius"}
|
||||||
|
aria-controls={radiusListId}
|
||||||
|
aria-activedescendant={
|
||||||
|
openDd === "radius" && radiusActive >= 0
|
||||||
|
? `${radiusListId}-opt-${radiusActive}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-label="Радиус анализа"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -787,19 +984,30 @@ export default function ParamsPanel({
|
||||||
padding: "6px 10px",
|
padding: "6px 10px",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: tokens.font.mono,
|
fontFamily: tokens.font.mono,
|
||||||
|
color: "inherit",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{radius}</span>
|
<span>{radius}</span>
|
||||||
<span style={{ color: tokens.muted2, fontSize: 9 }}>▼</span>
|
<span style={{ color: tokens.muted2, fontSize: 9 }}>▼</span>
|
||||||
</div>
|
</button>
|
||||||
{openDd === "radius" && (
|
{openDd === "radius" && (
|
||||||
<div style={radiusPanel}>
|
<div
|
||||||
{RADIUS_OPTIONS.map((o) => (
|
style={radiusPanel}
|
||||||
|
role="listbox"
|
||||||
|
id={radiusListId}
|
||||||
|
aria-label="Радиус анализа"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{RADIUS_OPTIONS.map((o, i) => (
|
||||||
<div
|
<div
|
||||||
key={o}
|
key={o}
|
||||||
className="pp-dd-opt"
|
className="pp-dd-opt"
|
||||||
|
role="option"
|
||||||
|
id={`${radiusListId}-opt-${i}`}
|
||||||
|
aria-selected={o === radius}
|
||||||
onClick={() => pick(setRadius)(o)}
|
onClick={() => pick(setRadius)(o)}
|
||||||
|
onMouseEnter={() => setRadiusActive(i)}
|
||||||
style={{
|
style={{
|
||||||
padding: "7px 10px",
|
padding: "7px 10px",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -808,6 +1016,8 @@ export default function ParamsPanel({
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
color: o === radius ? tokens.accent : tokens.ink,
|
color: o === radius ? tokens.accent : tokens.ink,
|
||||||
fontWeight: o === radius ? 600 : 400,
|
fontWeight: o === radius ? 600 : 400,
|
||||||
|
background:
|
||||||
|
i === radiusActive ? "rgba(46,139,255,.1)" : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{o}
|
{o}
|
||||||
|
|
@ -863,16 +1073,26 @@ export default function ParamsPanel({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div style={hintLabel}>АДРЕС (YANDEX / NOMINATIM)</div>
|
<label
|
||||||
|
htmlFor="pp-address"
|
||||||
|
style={{ ...hintLabel, display: "block" }}
|
||||||
|
>
|
||||||
|
АДРЕС (YANDEX / NOMINATIM)
|
||||||
|
</label>
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
{/* Address autocomplete (geocode suggest, ЕКБ viewbox). Typed text
|
{/* Address autocomplete (geocode suggest, ЕКБ viewbox). Typed text
|
||||||
is debounced into `addressQuery`; the dropdown below mirrors the
|
is debounced into `addressQuery`; the dropdown below mirrors the
|
||||||
HUD <Dd> panel styling. Picking an item fills the full address
|
HUD <Dd> panel styling. Picking an item fills the full address
|
||||||
and captures lat/lon for the estimate payload. */}
|
and captures lat/lon for the estimate payload. */}
|
||||||
<input
|
<input
|
||||||
|
id="pp-address"
|
||||||
className="pp-input"
|
className="pp-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={address}
|
value={address}
|
||||||
|
aria-invalid={fieldErrors.address ? true : undefined}
|
||||||
|
aria-describedby={
|
||||||
|
fieldErrors.address ? "pp-address-err" : undefined
|
||||||
|
}
|
||||||
onChange={(e) => handleAddressChange(e.target.value)}
|
onChange={(e) => handleAddressChange(e.target.value)}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
if (address.trim().length >= 3) setSuggestOpen(true);
|
if (address.trim().length >= 3) setSuggestOpen(true);
|
||||||
|
|
@ -937,7 +1157,11 @@ export default function ParamsPanel({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{fieldErrors.address && (
|
{fieldErrors.address && (
|
||||||
<div style={{ ...errorText, marginTop: 4 }} role="alert">
|
<div
|
||||||
|
id="pp-address-err"
|
||||||
|
style={{ ...errorText, marginTop: 4 }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{fieldErrors.address}
|
{fieldErrors.address}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -946,12 +1170,17 @@ export default function ParamsPanel({
|
||||||
|
|
||||||
<div style={fieldRow}>
|
<div style={fieldRow}>
|
||||||
<div>
|
<div>
|
||||||
<div style={hintLabel}>ПЛОЩАДЬ, М²</div>
|
<label htmlFor="pp-area" style={{ ...hintLabel, display: "block" }}>
|
||||||
|
ПЛОЩАДЬ, М²
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="pp-area"
|
||||||
className="pp-input"
|
className="pp-input"
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
value={area}
|
value={area}
|
||||||
|
aria-invalid={fieldErrors.area ? true : undefined}
|
||||||
|
aria-describedby={fieldErrors.area ? "pp-area-err" : undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setArea(e.target.value.replace(/[^\d.,]/g, ""));
|
setArea(e.target.value.replace(/[^\d.,]/g, ""));
|
||||||
if (fieldErrors.area)
|
if (fieldErrors.area)
|
||||||
|
|
@ -968,7 +1197,11 @@ export default function ParamsPanel({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{fieldErrors.area && (
|
{fieldErrors.area && (
|
||||||
<div style={{ ...errorText, marginTop: 4 }} role="alert">
|
<div
|
||||||
|
id="pp-area-err"
|
||||||
|
style={{ ...errorText, marginTop: 4 }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{fieldErrors.area}
|
{fieldErrors.area}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -984,6 +1217,7 @@ export default function ParamsPanel({
|
||||||
mono
|
mono
|
||||||
triggerFontSize={14}
|
triggerFontSize={14}
|
||||||
optionFontSize={13}
|
optionFontSize={13}
|
||||||
|
ariaLabel="Комнат"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -991,10 +1225,13 @@ export default function ParamsPanel({
|
||||||
<div style={fieldRow}>
|
<div style={fieldRow}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<span style={hintLabel}>ЭТАЖ</span>
|
<label htmlFor="pp-floor" style={hintLabel}>
|
||||||
|
ЭТАЖ
|
||||||
|
</label>
|
||||||
<span style={optHint}>если знаете</span>
|
<span style={optHint}>если знаете</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
id="pp-floor"
|
||||||
className="pp-input"
|
className="pp-input"
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
|
|
@ -1009,10 +1246,13 @@ export default function ParamsPanel({
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<span style={hintLabel}>ВСЕГО ЭТАЖЕЙ</span>
|
<label htmlFor="pp-total-floors" style={hintLabel}>
|
||||||
|
ВСЕГО ЭТАЖЕЙ
|
||||||
|
</label>
|
||||||
<span style={optHint}>если знаете</span>
|
<span style={optHint}>если знаете</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
id="pp-total-floors"
|
||||||
className="pp-input"
|
className="pp-input"
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
|
|
@ -1043,10 +1283,13 @@ export default function ParamsPanel({
|
||||||
<div style={fieldRow}>
|
<div style={fieldRow}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<span style={hintLabel}>ГОД ПОСТРОЙКИ</span>
|
<label htmlFor="pp-year" style={hintLabel}>
|
||||||
|
ГОД ПОСТРОЙКИ
|
||||||
|
</label>
|
||||||
<span style={optHint}>опц.</span>
|
<span style={optHint}>опц.</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
id="pp-year"
|
||||||
className="pp-input"
|
className="pp-input"
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
|
|
@ -1073,6 +1316,7 @@ export default function ParamsPanel({
|
||||||
mono={false}
|
mono={false}
|
||||||
triggerFontSize={13}
|
triggerFontSize={13}
|
||||||
optionFontSize={12}
|
optionFontSize={12}
|
||||||
|
ariaLabel="Тип дома"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1092,23 +1336,40 @@ export default function ParamsPanel({
|
||||||
mono={false}
|
mono={false}
|
||||||
triggerFontSize={13}
|
triggerFontSize={13}
|
||||||
optionFontSize={12}
|
optionFontSize={12}
|
||||||
|
ariaLabel="Состояние ремонта"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={hintLabel}>БАЛКОН / ЛОДЖИЯ</div>
|
<div style={hintLabel}>БАЛКОН / ЛОДЖИЯ</div>
|
||||||
<div style={{ height: 40, display: "flex", gap: 6 }}>
|
<div
|
||||||
<div
|
role="radiogroup"
|
||||||
|
aria-label="Балкон или лоджия"
|
||||||
|
style={{ height: 40, display: "flex", gap: 6 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={balcony === false}
|
||||||
|
tabIndex={balcony === false || balcony === null ? 0 : -1}
|
||||||
|
ref={balNoRef}
|
||||||
onClick={() => setBalcony(false)}
|
onClick={() => setBalcony(false)}
|
||||||
|
onKeyDown={handleBalconyKey}
|
||||||
style={{ ...balBase, ...(balcony === false ? balOn : balOff) }}
|
style={{ ...balBase, ...(balcony === false ? balOn : balOff) }}
|
||||||
>
|
>
|
||||||
НЕТ
|
НЕТ
|
||||||
</div>
|
</button>
|
||||||
<div
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={balcony === true}
|
||||||
|
tabIndex={balcony === true ? 0 : -1}
|
||||||
|
ref={balYesRef}
|
||||||
onClick={() => setBalcony(true)}
|
onClick={() => setBalcony(true)}
|
||||||
|
onKeyDown={handleBalconyKey}
|
||||||
style={{ ...balBase, ...(balcony === true ? balOn : balOff) }}
|
style={{ ...balBase, ...(balcony === true ? balOn : balOff) }}
|
||||||
>
|
>
|
||||||
ДА
|
ДА
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1136,6 +1397,7 @@ export default function ParamsPanel({
|
||||||
triggerColor={tokens.hint}
|
triggerColor={tokens.hint}
|
||||||
caretColor={tokens.muted4}
|
caretColor={tokens.muted4}
|
||||||
disabled
|
disabled
|
||||||
|
ariaLabel="CRM — для менеджера"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue