@sberbusiness/triplex-next-mcp-server 0.24.0-beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -0
- package/bundle/.gitkeep +0 -0
- package/bundle/components/AbstractTree.md +198 -0
- package/bundle/components/AccordionBase.md +227 -0
- package/bundle/components/Alert.md +317 -0
- package/bundle/components/Amount.md +225 -0
- package/bundle/components/AmountField.md +289 -0
- package/bundle/components/Avatar.md +209 -0
- package/bundle/components/Badge.md +277 -0
- package/bundle/components/Body.md +167 -0
- package/bundle/components/Button.md +230 -0
- package/bundle/components/ButtonDropdown.md +159 -0
- package/bundle/components/ButtonIcon.md +122 -0
- package/bundle/components/Calendar.md +445 -0
- package/bundle/components/Card.md +374 -0
- package/bundle/components/CarouselExtended.md +250 -0
- package/bundle/components/Checkbox.md +126 -0
- package/bundle/components/CheckboxTree.md +280 -0
- package/bundle/components/CheckboxTreeExtended.md +321 -0
- package/bundle/components/CheckboxXGroup.md +68 -0
- package/bundle/components/CheckboxYGroup.md +67 -0
- package/bundle/components/Chip.md +404 -0
- package/bundle/components/ChipGroup.md +179 -0
- package/bundle/components/Col.md +221 -0
- package/bundle/components/CollapsableTree.md +255 -0
- package/bundle/components/Confirm.md +343 -0
- package/bundle/components/DateField.md +371 -0
- package/bundle/components/DatePickerExtended.md +342 -0
- package/bundle/components/DateRange.md +275 -0
- package/bundle/components/DesignTokens.md +385 -0
- package/bundle/components/Divider.md +196 -0
- package/bundle/components/DocumentNumberEdit.md +280 -0
- package/bundle/components/Dropdown.md +433 -0
- package/bundle/components/Ellipsis.md +208 -0
- package/bundle/components/EmptyView.md +99 -0
- package/bundle/components/ExpandAnimation.md +281 -0
- package/bundle/components/Footer.md +339 -0
- package/bundle/components/FormField.md +326 -0
- package/bundle/components/FormGroup.md +99 -0
- package/bundle/components/Gap.md +113 -0
- package/bundle/components/Header.md +280 -0
- package/bundle/components/HelpBox.md +154 -0
- package/bundle/components/IconWrapper.md +145 -0
- package/bundle/components/Island.md +197 -0
- package/bundle/components/IslandAccordion.md +230 -0
- package/bundle/components/IslandWidget.md +233 -0
- package/bundle/components/KeyDownListener.md +157 -0
- package/bundle/components/LightBox.md +322 -0
- package/bundle/components/Link.md +161 -0
- package/bundle/components/List.md +247 -0
- package/bundle/components/ListMaster.md +204 -0
- package/bundle/components/Loader.md +161 -0
- package/bundle/components/LoaderScreen.md +134 -0
- package/bundle/components/Marker.md +127 -0
- package/bundle/components/MarkerStatus.md +148 -0
- package/bundle/components/MediaWidth.md +169 -0
- package/bundle/components/MobileView.md +120 -0
- package/bundle/components/ModalWindow.md +308 -0
- package/bundle/components/MonthYearField.md +168 -0
- package/bundle/components/MultiselectField.md +213 -0
- package/bundle/components/Notification.md +233 -0
- package/bundle/components/NumberField.md +178 -0
- package/bundle/components/OrderedList.md +169 -0
- package/bundle/components/Overlay.md +237 -0
- package/bundle/components/Page.md +246 -0
- package/bundle/components/Pagination.md +298 -0
- package/bundle/components/Portal.md +119 -0
- package/bundle/components/Radio.md +174 -0
- package/bundle/components/Row.md +125 -0
- package/bundle/components/SMSField.md +160 -0
- package/bundle/components/SegmentedControl.md +235 -0
- package/bundle/components/SelectExtendedField.md +322 -0
- package/bundle/components/SelectField.md +213 -0
- package/bundle/components/Skeleton.md +129 -0
- package/bundle/components/Slider.md +157 -0
- package/bundle/components/SliderExtended.md +210 -0
- package/bundle/components/SliderRange.md +160 -0
- package/bundle/components/SmallInput.md +101 -0
- package/bundle/components/Spoiler.md +121 -0
- package/bundle/components/StatusTracker.md +151 -0
- package/bundle/components/Step.md +121 -0
- package/bundle/components/Stepper.md +163 -0
- package/bundle/components/Suggest.md +160 -0
- package/bundle/components/SuggestField.md +157 -0
- package/bundle/components/SwipeableArea.md +144 -0
- package/bundle/components/Table.md +199 -0
- package/bundle/components/Tabs.md +152 -0
- package/bundle/components/TabsExtended.md +273 -0
- package/bundle/components/TabsLine.md +189 -0
- package/bundle/components/Tag.md +135 -0
- package/bundle/components/TagColor.md +121 -0
- package/bundle/components/TagGroup.md +100 -0
- package/bundle/components/TextField.md +152 -0
- package/bundle/components/TextareaField.md +124 -0
- package/bundle/components/ThemeProvider.md +181 -0
- package/bundle/components/Tooltip.md +254 -0
- package/bundle/components/TopOverlay.md +145 -0
- package/bundle/components/TreeView.md +224 -0
- package/bundle/components/Triggers.md +121 -0
- package/bundle/guides/codestyle.md +205 -0
- package/bundle/guides/commits.md +166 -0
- package/bundle/guides/context.md +277 -0
- package/bundle/guides/stories.md +618 -0
- package/bundle/guides/template.md +154 -0
- package/bundle/guides/tests.md +301 -0
- package/bundle/icons.json +4156 -0
- package/bundle/manifest.json +796 -0
- package/bundle/release-notes/1.1.0.mdx +26 -0
- package/bundle/release-notes/1.1.1.mdx +14 -0
- package/bundle/release-notes/1.10.0.mdx +43 -0
- package/bundle/release-notes/1.11.0.mdx +19 -0
- package/bundle/release-notes/1.12.0.mdx +21 -0
- package/bundle/release-notes/1.13.0.mdx +43 -0
- package/bundle/release-notes/1.14.0.mdx +29 -0
- package/bundle/release-notes/1.15.0.mdx +56 -0
- package/bundle/release-notes/1.16.0.mdx +55 -0
- package/bundle/release-notes/1.17.0.mdx +15 -0
- package/bundle/release-notes/1.18.0.mdx +24 -0
- package/bundle/release-notes/1.19.0.mdx +48 -0
- package/bundle/release-notes/1.19.1.mdx +9 -0
- package/bundle/release-notes/1.2.0.mdx +80 -0
- package/bundle/release-notes/1.20.0.mdx +48 -0
- package/bundle/release-notes/1.21.0.mdx +66 -0
- package/bundle/release-notes/1.22.0.mdx +75 -0
- package/bundle/release-notes/1.23.0.mdx +50 -0
- package/bundle/release-notes/1.24.0.mdx +59 -0
- package/bundle/release-notes/1.25.0.mdx +52 -0
- package/bundle/release-notes/1.26.0.mdx +11 -0
- package/bundle/release-notes/1.3.0.mdx +21 -0
- package/bundle/release-notes/1.4.0.mdx +37 -0
- package/bundle/release-notes/1.5.0.mdx +24 -0
- package/bundle/release-notes/1.6.0.mdx +37 -0
- package/bundle/release-notes/1.6.1.mdx +11 -0
- package/bundle/release-notes/1.7.0.mdx +29 -0
- package/bundle/release-notes/1.8.0.mdx +38 -0
- package/bundle/release-notes/1.9.0.mdx +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +324 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: DatePickerExtended
|
|
3
|
+
category: Internal
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [DateField, DateRange, MonthYearField, Calendar, Dropdown, FocusTrap]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: ""
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# DatePickerExtended
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Внутренняя "база" для всех полей выбора даты: объединяет `Dropdown`
|
|
17
|
+
(с desktop + mobile веткой), `Calendar` (внутри dropdown'а) и
|
|
18
|
+
`FocusTrap` (клавиатурная навигация). Управляет открытием/закрытием
|
|
19
|
+
dropdown'а (controlled/uncontrolled внутри себя — state), ловит
|
|
20
|
+
`Esc` для закрытия, отличает "мышиное" взаимодействие от "клавиатурного"
|
|
21
|
+
(флаг `mouseUsedRef`) для настройки `FocusTrap`.
|
|
22
|
+
|
|
23
|
+
Публично импортируется потребителями вроде `DateField`, `DateRange`,
|
|
24
|
+
`MonthYearField`, но **render-props-интерфейс** (`renderTarget`,
|
|
25
|
+
`renderDropdownHeaderTarget`) делает его сложным для прямого
|
|
26
|
+
использования в приложениях — обычно его оборачивают.
|
|
27
|
+
|
|
28
|
+
Используй когда: строишь новый компонент выбора даты со своим target'ом
|
|
29
|
+
и своей валидацией — DatePickerExtended даёт dropdown + calendar +
|
|
30
|
+
focus management бесплатно.
|
|
31
|
+
Не используй как замену `DateField`/`DateRange`/`MonthYearField` —
|
|
32
|
+
бери эти готовые обёртки.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Файловая структура
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
src/components/DatePickerExtended/
|
|
40
|
+
├── DatePickerExtended.tsx # forwardRef, root
|
|
41
|
+
├── DatePickerExtendedContext.tsx # Context { dropdownOpen, mouseUsedRef, setDropdownOpen }
|
|
42
|
+
├── DatePickerExtendedDropdown.tsx # forwardRef, обёртка Dropdown + FocusTrap + mobile ветка
|
|
43
|
+
└── styles/DatePickerExtended.module.less # Только .datePickerExtendedMobileDropdown { max-height: 100vh }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Особенности:
|
|
47
|
+
- **Нет `index.ts`** — импортируется как `import { DatePickerExtended } from ".../DatePickerExtended/DatePickerExtended"`.
|
|
48
|
+
Это намеренно — компонент считается "полупубличным" (используется
|
|
49
|
+
внутри библиотеки и потребителями при необходимости, но без красивого
|
|
50
|
+
barrel-a).
|
|
51
|
+
- **Нет `__tests__`** — покрывается через тесты `DateField`.
|
|
52
|
+
- **Нет собственных stories** — только через обёртки.
|
|
53
|
+
- **Почти нет LESS** — один класс для мобильного max-height.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Варианты и props
|
|
58
|
+
|
|
59
|
+
### `IDatePickerExtendedProps`
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">,
|
|
63
|
+
Pick<IDropdownProps, "alignment">,
|
|
64
|
+
ICalendarProps
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Рендер-пропсы (обязательные):**
|
|
68
|
+
|
|
69
|
+
| Prop | Тип | Описание |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `renderTarget` | `() => React.ReactNode` | Target (поле ввода) на десктопе. |
|
|
72
|
+
| `renderDropdownHeaderTarget` | `() => React.ReactNode` | Target в заголовке dropdown'а в мобильном режиме (обычно тот же input, но в другом контейнере). |
|
|
73
|
+
|
|
74
|
+
**Dropdown:**
|
|
75
|
+
|
|
76
|
+
| Prop | Тип | Default | Описание |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `alignment` | `EDropdownAlignment` | `LEFT` | Выравнивание dropdown'а относительно target'а. |
|
|
79
|
+
| `focusTrapProps` | `FocusTrapProps` | — | Прокидывается в `FocusTrap` (`focus-trap-react`). |
|
|
80
|
+
| `onDropdownOpen` | `() => void` | — | Callback при открытии. |
|
|
81
|
+
| `onDropdownClose` | `() => void` | — | Callback при закрытии. |
|
|
82
|
+
|
|
83
|
+
**Calendar (все пропсы `ICalendarProps` форвардятся):** `pickType`,
|
|
84
|
+
`format`, `defaultViewDate`, `pickedDate`, `limitRange`, `markedDays`,
|
|
85
|
+
`disabledDays`, `reversedPick`, `dayHtmlAttributes`, `monthHtmlAttributes`,
|
|
86
|
+
`yearHtmlAttributes`, `prevButtonProps`, `nextButtonProps`, `viewButtonProps`,
|
|
87
|
+
`todayButtonProps`, `onDateChange`, `onPageChange`, `onViewChange`.
|
|
88
|
+
|
|
89
|
+
**HTMLDivAttributes:** все, кроме `onChange` (конфликт с Calendar).
|
|
90
|
+
|
|
91
|
+
### `DatePickerExtendedContext`
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
interface IDatePickerExtendedContext {
|
|
95
|
+
dropdownOpen: boolean;
|
|
96
|
+
mouseUsedRef: React.MutableRefObject<boolean>;
|
|
97
|
+
setDropdownOpen: (open: boolean) => void;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Передаётся в `renderTarget` через контекст (target живёт в другой ветке
|
|
102
|
+
дерева). `DateFieldTarget` и аналоги используют `useContext` для
|
|
103
|
+
управления dropdown'ом из input'а.
|
|
104
|
+
|
|
105
|
+
### `DatePickerExtendedDropdown` — внутренний compound
|
|
106
|
+
|
|
107
|
+
`IDatePickerExtendedDropdownProps extends IDropdownProps` +
|
|
108
|
+
`renderCalendar`, `renderHeaderTarget`, `focusTrapProps`.
|
|
109
|
+
Не экспортируется публично.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Ключевые особенности реализации
|
|
114
|
+
|
|
115
|
+
### `mouseUsedRef` — отличение мышь vs клавиатура
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const mouseUsedRef = useRef(false);
|
|
119
|
+
|
|
120
|
+
const handleMouseDown = (event) => {
|
|
121
|
+
mouseUsedRef.current = true;
|
|
122
|
+
onMouseDown?.(event);
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Сбрасывается при закрытии dropdown'а (в `useEffect`, ветка else).
|
|
127
|
+
Используется в `FocusTrap`:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<FocusTrap
|
|
131
|
+
focusTrapOptions={{
|
|
132
|
+
initialFocus: !mouseUsedRef.current && undefined,
|
|
133
|
+
returnFocusOnDeactivate: !mouseUsedRef.current,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Зачем:** если пользователь кликнул по полю мышью → не ставить
|
|
139
|
+
программный initialFocus внутрь dropdown'а (пользователь не хочет,
|
|
140
|
+
чтобы фокус "прыгал") и не возвращать фокус на target при закрытии.
|
|
141
|
+
Если клавиатурой (Enter/Space на input) → focus-trap работает, по Esc
|
|
142
|
+
фокус вернётся на input.
|
|
143
|
+
|
|
144
|
+
**Это UX-тонкость**, нельзя просто удалить `mouseUsedRef` — пострадает
|
|
145
|
+
клавиатурная доступность.
|
|
146
|
+
|
|
147
|
+
### Outside mousedown → закрытие dropdown'а
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const handleOutsideMouseDown = (event: MouseEvent) => {
|
|
152
|
+
if (
|
|
153
|
+
!containerRef.current?.contains(event.target as Node) &&
|
|
154
|
+
!dropdownRef.current?.contains(event.target as Node)
|
|
155
|
+
) {
|
|
156
|
+
setDropdownOpen(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
if (dropdownOpen) {
|
|
160
|
+
document.addEventListener("mousedown", handleOutsideMouseDown);
|
|
161
|
+
return () => document.removeEventListener("mousedown", handleOutsideMouseDown);
|
|
162
|
+
} else {
|
|
163
|
+
mouseUsedRef.current = false;
|
|
164
|
+
}
|
|
165
|
+
}, [dropdownOpen]);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Проверяет два контейнера** — корневой (`containerRef`, target) и
|
|
169
|
+
`dropdownRef` (сам dropdown). Если клик не в них обоих — закрыть.
|
|
170
|
+
`dropdownRef` нужен потому что Dropdown рендерится через Portal и
|
|
171
|
+
не является DOM-потомком `containerRef`.
|
|
172
|
+
|
|
173
|
+
**`mouseUsedRef` сбрасывается в else-ветке** — при каждом закрытии
|
|
174
|
+
флаг становится `false`, чтобы следующее открытие корректно решало,
|
|
175
|
+
как вести себя с focus trap'ом.
|
|
176
|
+
|
|
177
|
+
### Esc → закрыть dropdown (локальный onKeyDown)
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const handleKeyDown = (event) => {
|
|
181
|
+
if (isKey(key, "ESCAPE")) {
|
|
182
|
+
if (dropdownOpen) setDropdownOpen(false);
|
|
183
|
+
}
|
|
184
|
+
onKeyDown?.(event);
|
|
185
|
+
};
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Esc обрабатывается на root div**, не на document. Нужно, чтобы Esc
|
|
189
|
+
внутри input'а (фокус в target) → закрыть dropdown. Не фильтруется
|
|
190
|
+
`event.defaultPrevented` — если потребитель уже обработал Esc, он
|
|
191
|
+
всё равно закроет dropdown. Это компромисс.
|
|
192
|
+
|
|
193
|
+
### `renderCalendar(adaptiveMode)` — две ветки
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
const renderMobileContent = () => (
|
|
197
|
+
<>
|
|
198
|
+
<DropdownMobileHeader controlButtons={<DropdownMobileClose onClick={...} />}>
|
|
199
|
+
{renderHeaderTarget()}
|
|
200
|
+
</DropdownMobileHeader>
|
|
201
|
+
<DropdownMobileBody>{renderCalendar(true)}</DropdownMobileBody>
|
|
202
|
+
</>
|
|
203
|
+
);
|
|
204
|
+
// desktop:
|
|
205
|
+
<FocusTrap ...>
|
|
206
|
+
<div role="presentation">{renderCalendar(false)}</div>
|
|
207
|
+
</FocusTrap>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**`adaptiveMode=true`** — календарь на мобильном (полноэкранный,
|
|
211
|
+
другие размеры и поведение).
|
|
212
|
+
**`adaptiveMode=false`** — десктопный компактный календарь в dropdown'е.
|
|
213
|
+
|
|
214
|
+
Calendar сам смотрит на `adaptiveMode` и меняет поведение.
|
|
215
|
+
|
|
216
|
+
### `onDateChange` → закрыть dropdown + вызвать внешний
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const handleChangeDate = (date: moment.Moment) => {
|
|
220
|
+
setDropdownOpen(false);
|
|
221
|
+
onDateChange(date);
|
|
222
|
+
};
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Выбор даты → dropdown закрывается автоматически. Это жёсткое
|
|
226
|
+
поведение, потребитель не может отключить. Если нужно "мультидневный
|
|
227
|
+
выбор" — Calendar не подходит, нужен другой компонент.
|
|
228
|
+
|
|
229
|
+
### `FocusTrap` с `clickOutsideDeactivates`
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
focusTrapOptions={{
|
|
233
|
+
clickOutsideDeactivates: true,
|
|
234
|
+
fallbackFocus: targetRef.current!,
|
|
235
|
+
...
|
|
236
|
+
}}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- `clickOutsideDeactivates` — клик вне → FocusTrap отпускает фокус,
|
|
240
|
+
но outside-handler всё равно закрывает dropdown. Это **защита**:
|
|
241
|
+
FocusTrap не борется с outside-handler'ом.
|
|
242
|
+
- `fallbackFocus: targetRef.current!` — если FocusTrap не может найти
|
|
243
|
+
focusable элемент внутри, поставить фокус на target (non-null
|
|
244
|
+
assertion обязателен потому что при первой активации targetRef
|
|
245
|
+
уже mounted).
|
|
246
|
+
|
|
247
|
+
### `role="dialog" aria-modal="true"` на Dropdown
|
|
248
|
+
|
|
249
|
+
Семантика модального диалога для содержимого (календарь). Screen reader
|
|
250
|
+
объявит как модал. **Jарко** (не опционально), `...rest` идёт в
|
|
251
|
+
Dropdown после `role`/`aria-modal`, но `role` и `aria-modal` не
|
|
252
|
+
подлежат перебиванию снаружи потребителя (IDatePickerExtendedProps не
|
|
253
|
+
принимает role).
|
|
254
|
+
|
|
255
|
+
### `data-tx={process.env.npm_package_version}` — есть
|
|
256
|
+
|
|
257
|
+
Стандарт библиотеки. Не удалять.
|
|
258
|
+
|
|
259
|
+
### `setRef` — double-duty ref
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const setRef = (node: HTMLDivElement | null) => {
|
|
263
|
+
containerRef.current = node;
|
|
264
|
+
if (typeof ref === "function") ref(node);
|
|
265
|
+
else if (ref) ref.current = node;
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Один DOM-узел = target для Dropdown (`targetRef`) + forwarded ref.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Инварианты
|
|
274
|
+
|
|
275
|
+
- **`forwardRef`** на root — обязателен (нужен для тулов).
|
|
276
|
+
- **`mouseUsedRef`** — не удалять. Управляет UX focus-trap'а.
|
|
277
|
+
- **Outside-handler проверяет оба ref'а** (containerRef + dropdownRef) —
|
|
278
|
+
не сводить к одному.
|
|
279
|
+
- **`mouseUsedRef.current = false` в else-ветке useEffect** — сбросить
|
|
280
|
+
флаг при каждом закрытии, иначе следующий цикл не получит корректный
|
|
281
|
+
initialFocus.
|
|
282
|
+
- **`role="dialog" aria-modal="true"`** на Dropdown — обязательно.
|
|
283
|
+
- **`clickOutsideDeactivates: true`** в FocusTrap — иначе outside-click
|
|
284
|
+
не закрывает dropdown (FocusTrap борется).
|
|
285
|
+
- **`setDropdownOpen(false)` в `handleChangeDate`** — выбор даты
|
|
286
|
+
закрывает dropdown. Неопциональное поведение.
|
|
287
|
+
- **Esc → закрытие независимо от defaultPrevented** — осознанное
|
|
288
|
+
решение. Не фильтровать.
|
|
289
|
+
- **Render-props `renderTarget` и `renderDropdownHeaderTarget`** —
|
|
290
|
+
единственный способ вставить target. Не принимать children.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Accessibility
|
|
295
|
+
|
|
296
|
+
- **`FocusTrap`** (`focus-trap-react`) — клавиатурный фокус заперт
|
|
297
|
+
внутри dropdown'а, Tab ходит по кнопкам календаря.
|
|
298
|
+
- **`role="dialog" aria-modal="true"`** на Dropdown — семантика модала.
|
|
299
|
+
- **Esc** — закрывает dropdown.
|
|
300
|
+
- **`initialFocus`/`returnFocusOnDeactivate`** — условно, только для
|
|
301
|
+
клавиатурного открытия. При мышином открытии фокус не трогается
|
|
302
|
+
(UX-паттерн).
|
|
303
|
+
- **`<div role="presentation">`** вокруг календаря — убирает лишнюю
|
|
304
|
+
семантику. Сам Calendar имеет свою ARIA.
|
|
305
|
+
- **Mobile** — собственная разметка с `DropdownMobileHeader`/`Body`,
|
|
306
|
+
включая кнопку закрытия (`DropdownMobileClose`).
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Дизайн-токены
|
|
311
|
+
|
|
312
|
+
**Нет собственных.** Dropdown и Calendar — источники всех визуальных
|
|
313
|
+
токенов.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Связанные компоненты
|
|
318
|
+
|
|
319
|
+
- `Dropdown` (`src/components/Dropdown`) — контейнер (Portal + positioning).
|
|
320
|
+
- `Calendar` (`src/components/Calendar`) — собственно выбор даты.
|
|
321
|
+
- `DropdownMobile*` (`src/components/Dropdown/mobile`) — мобильные
|
|
322
|
+
подкомпоненты (Header/Body/Close).
|
|
323
|
+
- `FocusTrap` (`focus-trap-react`) — внешний пакет для захвата фокуса.
|
|
324
|
+
- `DateField` (`src/components/DateField`) — основной потребитель.
|
|
325
|
+
- `DateRange` (`src/components/DateRange`) — потребитель для диапазона.
|
|
326
|
+
- `MonthYearField` (`src/components/MonthYearField`) — потребитель
|
|
327
|
+
для месяца/года.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Stories
|
|
332
|
+
|
|
333
|
+
**Нет собственных stories.** Документируется косвенно через
|
|
334
|
+
`DateField`/`DateRange`/`MonthYearField`.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## История изменений
|
|
339
|
+
|
|
340
|
+
| Дата | Изменение |
|
|
341
|
+
|---|---|
|
|
342
|
+
| 2026-04-18 | Создан документ |
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: DateRange
|
|
3
|
+
category: Form
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [DateField, DatePickerExtended, Calendar, MonthYearField]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: stories/DateRange/DateRange.stories.tsx
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# DateRange
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Layout-контейнер для выбора **диапазона дат** (от … до) с опциональными
|
|
17
|
+
кнопками "сдвинуть диапазон назад/вперёд" на фиксированную величину
|
|
18
|
+
(день/неделя/месяц/квартал/год). Сам **не рендерит пикеры** — принимает
|
|
19
|
+
их через render-props (`renderPickerFrom`, `renderPickerTo`,
|
|
20
|
+
`renderButtonBack`, `renderButtonForward`), потребитель обычно
|
|
21
|
+
передаёт два `DateField`.
|
|
22
|
+
|
|
23
|
+
Автоматически **валидирует пересечение**: если пользователь поставил
|
|
24
|
+
`from > to`, компонент очищает `to` (или наоборот очищает `from`, если
|
|
25
|
+
пользователь ставит `to < from`).
|
|
26
|
+
|
|
27
|
+
Используй когда: нужен диапазон дат в форме (например, фильтр периода).
|
|
28
|
+
Не используй когда: нужна **одна** дата — бери `DateField`; нужен выбор
|
|
29
|
+
одного **периода** (месяц/квартал) — `MonthYearField`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Файловая структура
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
src/components/DateRange/
|
|
37
|
+
├── DateRange.tsx # FC, НЕ forwardRef
|
|
38
|
+
├── enums.ts # EDateRangeShiftUnit (day/week/month/quarter/year)
|
|
39
|
+
├── index.ts # Barrel
|
|
40
|
+
├── styles/DateRange.module.less # .dateRange flex + .separator 20x20 + .dateRangeButton z-index: 1
|
|
41
|
+
└── __tests__/DateRange.test.tsx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Особенности:
|
|
45
|
+
- **НЕ `forwardRef`** — FC. Ref-форвардинг не реализован. Известный
|
|
46
|
+
AI-ready debt.
|
|
47
|
+
- **Render-props-first** — `renderPickerFrom`, `renderPickerTo`,
|
|
48
|
+
`renderButtonBack`, `renderButtonForward` — все 4 обязательны.
|
|
49
|
+
- **`moment.js`** для арифметики сдвига диапазона.
|
|
50
|
+
- **Тесты есть** — `__tests__/DateRange.test.tsx` покрывает валидацию
|
|
51
|
+
пересечения и сдвиги.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Варианты и props
|
|
56
|
+
|
|
57
|
+
### `IDateRangeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange">`
|
|
58
|
+
|
|
59
|
+
| Prop | Тип | Default | Описание |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `value` | `TDateRangeValue = [string, string]` | — | Диапазон `[from, to]` в формате `YYYY-MM-DD`. |
|
|
62
|
+
| `onChange` | `(value: [string, string]) => void` | — | Вызывается при изменении любой даты или при сдвиге. |
|
|
63
|
+
| `shiftAmount` | `number` | `1` | Величина сдвига диапазона. |
|
|
64
|
+
| `shiftUnit` | `EDateRangeShiftUnit` | `MONTH` | Единица сдвига (`"day" \| "week" \| "month" \| "quarter" \| "year"` — значения moment-совместимы). |
|
|
65
|
+
| `hideNavigation` | `boolean` | `false` | Скрыть кнопки сдвига. |
|
|
66
|
+
| `renderPickerFrom` | `(props: IDateRangePickerProvideProps) => ReactNode` | — | Обязателен. Рендерит поле "от". |
|
|
67
|
+
| `renderPickerTo` | `(props: IDateRangePickerProvideProps) => ReactNode` | — | Обязателен. Рендерит поле "до". |
|
|
68
|
+
| `renderButtonBack` | `(props: IDateRangeButtonProvideProps) => ReactNode` | — | Обязателен. Рендерит кнопку "назад". |
|
|
69
|
+
| `renderButtonForward` | `(props: IDateRangeButtonProvideProps) => ReactNode` | — | Обязателен. Рендерит кнопку "вперёд". |
|
|
70
|
+
| `className`, `...HTMLDivAttributes` | — | — | Стандартные. |
|
|
71
|
+
|
|
72
|
+
### Provide-props
|
|
73
|
+
|
|
74
|
+
**`IDateRangePickerProvideProps`:**
|
|
75
|
+
```ts
|
|
76
|
+
{ value: string; onChange: (value: string) => void; }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**`IDateRangeButtonProvideProps`:**
|
|
80
|
+
```ts
|
|
81
|
+
{
|
|
82
|
+
children: ReactNode; // стрелка-иконка Caretleft/rightStrokeSrvIcon20
|
|
83
|
+
className: string; // .dateRangeButton + .disabled при пустом диапазоне
|
|
84
|
+
onClick: () => void; // shiftRangeBack / shiftRangeForward
|
|
85
|
+
disabled: boolean; // true если start или end пустые
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `TDateRangeValue = [string, string]`
|
|
90
|
+
|
|
91
|
+
Кортеж из двух строк. Пустая строка = "дата не задана". Это важно: **не
|
|
92
|
+
`undefined`, а именно `""`**. Потребитель должен всегда передавать
|
|
93
|
+
массив длиной 2.
|
|
94
|
+
|
|
95
|
+
### `EDateRangeShiftUnit`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
enum EDateRangeShiftUnit {
|
|
99
|
+
DAY = "day",
|
|
100
|
+
WEEK = "week",
|
|
101
|
+
MONTH = "month",
|
|
102
|
+
QUARTER = "quarter",
|
|
103
|
+
YEAR = "year",
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Значения — строки, совместимые с `moment().add(n, unit)`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Ключевые особенности реализации
|
|
112
|
+
|
|
113
|
+
### Валидация пересечения: очистка при инверсии
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const handleChangePickerFrom = (date: string) => {
|
|
117
|
+
if (!date || !end || date <= end) {
|
|
118
|
+
onChange([date, end]);
|
|
119
|
+
} else {
|
|
120
|
+
onChange([date, ""]); // from > to → чистим to
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleChangePickerTo = (date: string) => {
|
|
125
|
+
if (!date || !start || date >= start) {
|
|
126
|
+
onChange([start, date]);
|
|
127
|
+
} else {
|
|
128
|
+
onChange(["", date]); // to < from → чистим from
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Важная деталь:** сравнение `date <= end` / `date >= start` — это
|
|
134
|
+
**лексикографическое** сравнение строк. Работает **только** для формата
|
|
135
|
+
`YYYY-MM-DD` (ISO). Для других форматов (`DD.MM.YYYY` и т. п.) сравнение
|
|
136
|
+
сломается.
|
|
137
|
+
|
|
138
|
+
**Инвариант:** `value` и всё, что DateRange возвращает через `onChange`,
|
|
139
|
+
строго в `YYYY-MM-DD`. Потребитель отвечает за форматирование в
|
|
140
|
+
собственных пикерах (`DateField` сам конвертирует через `format` проп,
|
|
141
|
+
но по умолчанию тоже `YYYY-MM-DD`).
|
|
142
|
+
|
|
143
|
+
### Сдвиг диапазона — `moment` арифметика
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const shiftRangeBack = () => {
|
|
147
|
+
if (!start || !end) return;
|
|
148
|
+
const momentStart = moment(start, dateFormatYYYYMMDD);
|
|
149
|
+
const momentEnd = moment(end, dateFormatYYYYMMDD);
|
|
150
|
+
onChange([
|
|
151
|
+
momentStart.subtract(shiftAmount, shiftUnit).format(dateFormatYYYYMMDD),
|
|
152
|
+
momentEnd.subtract(shiftAmount, shiftUnit).format(dateFormatYYYYMMDD),
|
|
153
|
+
]);
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Защита:** если start или end пустые → no-op. Кнопка тоже
|
|
158
|
+
`disabled={!(start && end)}`.
|
|
159
|
+
|
|
160
|
+
**Сдвигаются обе границы на одно и то же значение** — не "растянуть
|
|
161
|
+
диапазон", а "сдвинуть целиком". Размер диапазона сохраняется.
|
|
162
|
+
|
|
163
|
+
**Граничный случай:** если `shiftUnit=MONTH` и `start=2026-01-31`, то
|
|
164
|
+
`moment.subtract(1, 'month') = 2025-12-31` (moment сам обрабатывает
|
|
165
|
+
конец месяца). Для шага `1 year + 29 feb` — может быть скачок на 1
|
|
166
|
+
день. Это поведение `moment` и осознанное допущение.
|
|
167
|
+
|
|
168
|
+
### Separator — `MinusStrokeSrvIcon20`
|
|
169
|
+
|
|
170
|
+
Между двумя пикерами рендерится иконка-минус 20x20 (`flex-shrink: 0`,
|
|
171
|
+
чтобы не сжималась).
|
|
172
|
+
|
|
173
|
+
### Кнопки сдвига — позиция z-index: 1
|
|
174
|
+
|
|
175
|
+
```less
|
|
176
|
+
.dateRangeButton { z-index: 1; }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Зачем:** в макете кнопки могут визуально перекрываться с soft-edge
|
|
180
|
+
полями пикеров (Dropdown может показываться поверх). z-index: 1 даёт
|
|
181
|
+
кнопкам приоритет над содержимым пикера при совпадении контекста.
|
|
182
|
+
**Не удалять** — без этого кнопки могут "уйти под" пикер.
|
|
183
|
+
|
|
184
|
+
### `disabled` = нет диапазона (пусто)
|
|
185
|
+
|
|
186
|
+
Кнопки сдвига становятся `disabled`, пока не выбраны **обе** даты.
|
|
187
|
+
Логично — не можем сдвинуть "не определённый" диапазон.
|
|
188
|
+
|
|
189
|
+
### `hideNavigation` — скрывает кнопки, не сам сдвиг
|
|
190
|
+
|
|
191
|
+
Внешний `shiftRange*` не публичные методы — доступ к сдвигам есть
|
|
192
|
+
только через UI-кнопки. Если `hideNavigation=true`, сдвиги недоступны.
|
|
193
|
+
|
|
194
|
+
Если нужны **кастомные триггеры сдвига** (например, кнопки в другом
|
|
195
|
+
месте UI), это не поддерживается — потребитель должен сам арифметику
|
|
196
|
+
`moment` реализовать и пушить новое значение.
|
|
197
|
+
|
|
198
|
+
### Нет forwardRef — известный debt
|
|
199
|
+
|
|
200
|
+
`React.FC` без ref-forwarding. Потребителю ref недоступен. Если нужен —
|
|
201
|
+
оборачивать в собственный div с ref (deviates от паттерна).
|
|
202
|
+
|
|
203
|
+
### `data-tx={process.env.npm_package_version}` — **нет**
|
|
204
|
+
|
|
205
|
+
Этот компонент **не** ставит data-tx. Исключение из общего правила
|
|
206
|
+
библиотеки. Возможно debt — при ревизии может быть добавлен.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Инварианты
|
|
211
|
+
|
|
212
|
+
- **`value` и `onChange` — в `YYYY-MM-DD`** (`dateFormatYYYYMMDD`).
|
|
213
|
+
Лексикографическое сравнение не будет работать в других форматах.
|
|
214
|
+
- **Все 4 render-props обязательны** — `renderPickerFrom`, `renderPickerTo`,
|
|
215
|
+
`renderButtonBack`, `renderButtonForward`.
|
|
216
|
+
- **Инверсия диапазона очищает противоположную границу** —
|
|
217
|
+
`from > to` → чистим `to`; `to < from` → чистим `from`. Не менять.
|
|
218
|
+
- **Сдвиг блокируется при пустом диапазоне** — `!start || !end` → no-op +
|
|
219
|
+
`disabled: true`.
|
|
220
|
+
- **`shiftUnit` значения совместимы с moment** — не добавлять значения,
|
|
221
|
+
не поддерживаемые `moment.add/subtract`.
|
|
222
|
+
- **Separator — иконка минуса 20x20** — не заменять на "–" текст
|
|
223
|
+
(не масштабируется).
|
|
224
|
+
- **`.dateRangeButton { z-index: 1 }`** — не удалять.
|
|
225
|
+
- **НЕТ forwardRef** — известный debt, не добавлять точечно.
|
|
226
|
+
- **НЕТ data-tx** — наследованный пропуск.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Accessibility
|
|
231
|
+
|
|
232
|
+
- **Кнопки "назад"/"вперёд"** — потребитель отвечает за `aria-label`
|
|
233
|
+
(через render-props). DateRange не задаёт `aria-label`.
|
|
234
|
+
- **`disabled`** — передаётся в render-props, обычно попадает на
|
|
235
|
+
`<button disabled>`.
|
|
236
|
+
- **Separator** — декоративная иконка, screen reader её не озвучит
|
|
237
|
+
(SVG без `role`/`aria-label`).
|
|
238
|
+
- **Связь полей from-to** — ARIA связи не выставлены. Если нужна
|
|
239
|
+
явная связь "это пара полей" — обёртывать в `<fieldset><legend>`.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Дизайн-токены
|
|
244
|
+
|
|
245
|
+
**Нет.** Layout-компонент, разметка flex + фиксированный separator.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Связанные компоненты
|
|
250
|
+
|
|
251
|
+
- `DateField` (`src/components/DateField`) — типичный `renderPickerFrom`/`To`.
|
|
252
|
+
- `DatePickerExtended` — низкоуровневый пикер.
|
|
253
|
+
- `MonthYearField` — близнец, но для выбора отдельного периода.
|
|
254
|
+
- `Button`/`ButtonIcon` — типично для `renderButtonBack`/`Forward`.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Stories
|
|
259
|
+
|
|
260
|
+
`stories/DateRange/DateRange.stories.tsx`
|
|
261
|
+
Примеры: `stories/DateRange/examples/`
|
|
262
|
+
|
|
263
|
+
Типовые stories:
|
|
264
|
+
- `Default` — два DateField + кнопки сдвига
|
|
265
|
+
- `HideNavigation` — без кнопок
|
|
266
|
+
- `ShiftUnits` — перебор DAY/WEEK/MONTH/QUARTER/YEAR
|
|
267
|
+
- `Playground` — интерактив
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## История изменений
|
|
272
|
+
|
|
273
|
+
| Дата | Изменение |
|
|
274
|
+
|---|---|
|
|
275
|
+
| 2026-04-18 | Создан документ |
|