@bytebrand/fe-ui-core-autobahn 1.0.162 → 1.0.164
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/locales/de/autobahn.json +2 -1
- package/locales/en/autobahn.json +2 -1
- package/package.json +30 -30
- package/ui/Dropdown.tsx +16 -8
- package/ui/index.ts +2 -0
- package/ui/scrollFieldIntoView.ts +41 -0
- package/ui/usePortalMenuPosition.ts +14 -12
package/locales/de/autobahn.json
CHANGED
package/locales/en/autobahn.json
CHANGED
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bytebrand/fe-ui-core-autobahn",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
|
|
5
|
-
"main": "index.ts",
|
|
6
|
-
"license": "UNLICENSED",
|
|
7
|
-
"peerDependencies": {
|
|
8
|
-
"react": "^18.0.0",
|
|
9
|
-
"react-dom": "^18.0.0"
|
|
10
|
-
},
|
|
11
|
-
"dependencies": {
|
|
12
|
-
"@bytebrand/car-schema-selectors": "^2.0.7",
|
|
13
|
-
"@bytebrand/fe-financing": "^5.14.20",
|
|
14
|
-
"classnames": "2.2.6",
|
|
15
|
-
"clsx": "^2.1.1",
|
|
16
|
-
"d3-format": "1.4.5",
|
|
17
|
-
"js-cookie": "^2.2.0",
|
|
18
|
-
"lodash": "^4.17.21",
|
|
19
|
-
"lucide-react": "^0.288.0",
|
|
20
|
-
"mobx-react": "^7.6.0",
|
|
21
|
-
"moment": "^2.29.1",
|
|
22
|
-
"qs": "^6.6.0",
|
|
23
|
-
"react-device-detect": "^2.1.2",
|
|
24
|
-
"react-grid-system": "^5.0.2",
|
|
25
|
-
"react-lazyload": "^3.2.0",
|
|
26
|
-
"react-scroll": "^1.9.3",
|
|
27
|
-
"react-slick": "^0.29.0",
|
|
28
|
-
"slick-carousel": "1.8.1"
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@bytebrand/fe-ui-core-autobahn",
|
|
3
|
+
"version": "1.0.164",
|
|
4
|
+
"description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"react": "^18.0.0",
|
|
9
|
+
"react-dom": "^18.0.0"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@bytebrand/car-schema-selectors": "^2.0.7",
|
|
13
|
+
"@bytebrand/fe-financing": "^5.14.20",
|
|
14
|
+
"classnames": "2.2.6",
|
|
15
|
+
"clsx": "^2.1.1",
|
|
16
|
+
"d3-format": "1.4.5",
|
|
17
|
+
"js-cookie": "^2.2.0",
|
|
18
|
+
"lodash": "^4.17.21",
|
|
19
|
+
"lucide-react": "^0.288.0",
|
|
20
|
+
"mobx-react": "^7.6.0",
|
|
21
|
+
"moment": "^2.29.1",
|
|
22
|
+
"qs": "^6.6.0",
|
|
23
|
+
"react-device-detect": "^2.1.2",
|
|
24
|
+
"react-grid-system": "^5.0.2",
|
|
25
|
+
"react-lazyload": "^3.2.0",
|
|
26
|
+
"react-scroll": "^1.9.3",
|
|
27
|
+
"react-slick": "^0.29.0",
|
|
28
|
+
"slick-carousel": "1.8.1"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/ui/Dropdown.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import * as ReactDOM from 'react-dom';
|
|
|
22
22
|
|
|
23
23
|
import Icon from './Icon';
|
|
24
24
|
import { usePortalMenuPosition } from './usePortalMenuPosition';
|
|
25
|
+
import { scrollFieldIntoView } from './scrollFieldIntoView';
|
|
25
26
|
|
|
26
27
|
export interface DropdownOption {
|
|
27
28
|
value: string;
|
|
@@ -189,15 +190,22 @@ const Dropdown: React.FunctionComponent<DropdownProps> = ({
|
|
|
189
190
|
if (allowCustomValue) setQuery(value || '');
|
|
190
191
|
}, [disabled, selectedIndex, allowCustomValue, value]);
|
|
191
192
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
193
|
+
// Scroll the trigger (searchable or not) to the top of the page on open, THEN — searchable
|
|
194
|
+
// only — focus the search input instead of leaving focus on the trigger. Every Dropdown shares
|
|
195
|
+
// this, not just searchable ones: on mobile a trigger sitting mid-page can open a menu that
|
|
196
|
+
// runs off the bottom of the viewport with no way to see the rest of it, same underlying
|
|
197
|
+
// problem a searchable trigger has with the keyboard eating the space below it. A plain
|
|
198
|
+
// `scrollIntoView({block:'center'})` used to run here (searchable-only) — "center" only moves
|
|
199
|
+
// the trigger when it's already near a viewport edge, so on a trigger that's roughly mid-page
|
|
200
|
+
// already (the common case) it was a near no-op, leaving a few hundred px of dead space above
|
|
201
|
+
// the trigger that the keyboard (searchable) or the menu's own bottom edge (any dropdown) then
|
|
202
|
+
// ran into.
|
|
196
203
|
React.useEffect(() => {
|
|
197
|
-
if (!open
|
|
198
|
-
if (rootRef.current
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
if (!open) return;
|
|
205
|
+
if (rootRef.current) scrollFieldIntoView(rootRef.current);
|
|
206
|
+
// searchRef is only ever attached in the searchable+open (editing) render branch, so this
|
|
207
|
+
// naturally no-ops for a non-searchable dropdown — nothing else to focus there, the trigger
|
|
208
|
+
// <button> already has focus from the click/keypress that opened it.
|
|
201
209
|
if (searchRef.current) {
|
|
202
210
|
searchRef.current.focus();
|
|
203
211
|
// pre-filled custom value (see openMenu) starts selected, so typing immediately
|
package/ui/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ export type { FormFieldProps } from './FormField';
|
|
|
39
39
|
export { default as Dropdown } from './Dropdown';
|
|
40
40
|
export type { DropdownProps, DropdownOption } from './Dropdown';
|
|
41
41
|
|
|
42
|
+
export { scrollFieldIntoView } from './scrollFieldIntoView';
|
|
43
|
+
|
|
42
44
|
export { default as Spec } from './Spec';
|
|
43
45
|
export type { SpecProps } from './Spec';
|
|
44
46
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Autobahn (auto.de redesign) — shared "scroll a just-opened/just-focused field to the top of the
|
|
2
|
+
// page" behaviour, used by Dropdown.tsx and by fe-main's DualRange/RateSlider (FilterContent.tsx)
|
|
3
|
+
// text fields on /search/detailed. On mobile, opening a dropdown's menu or focusing a text field
|
|
4
|
+
// brings the on-screen keyboard up and/or needs room for a popover below the field — if the field
|
|
5
|
+
// is sitting mid-page, there may not be enough space left below it once the keyboard/menu takes
|
|
6
|
+
// its share. Scrolling the field to the very top of the page first maximizes what's available.
|
|
7
|
+
//
|
|
8
|
+
// Extracted out of Dropdown.tsx so it can be shared verbatim with fe-main's non-Dropdown form
|
|
9
|
+
// fields (DualRange/RateSlider's text inputs) — same problem, same fix, one place to keep them
|
|
10
|
+
// from drifting apart. See Dropdown.tsx's own call site for the original reasoning this was
|
|
11
|
+
// pulled from.
|
|
12
|
+
export function scrollFieldIntoView(field: HTMLElement): void {
|
|
13
|
+
if (typeof window === 'undefined') return;
|
|
14
|
+
// Mobile-only: the on-screen-keyboard/popover space problem this fixes doesn't exist on
|
|
15
|
+
// desktop, where there's no keyboard pushing the viewport around. Same breakpoint + matchMedia
|
|
16
|
+
// pattern as Modal.tsx/CardCarousel.tsx elsewhere in this lib.
|
|
17
|
+
if (window.matchMedia('(min-width: 601px)').matches) return;
|
|
18
|
+
// Anchor on whichever is higher up: the field itself, or a short field label/caption sitting
|
|
19
|
+
// directly above it (own previous sibling, or — the common "label above a row of controls"
|
|
20
|
+
// shape, e.g. "Marke & Modell" above the Make/Model row on /search/detailed — the wrapping
|
|
21
|
+
// row's previous sibling). Checked by height, not by class name, so this works for ANY
|
|
22
|
+
// caller's label without this needing to know that call site's specific markup.
|
|
23
|
+
const MAX_LABEL_HEIGHT = 30;
|
|
24
|
+
const asLabel = (el: Element | null): HTMLElement | null => {
|
|
25
|
+
if (!(el instanceof HTMLElement)) return null;
|
|
26
|
+
const h = el.getBoundingClientRect().height;
|
|
27
|
+
return h > 0 && h <= MAX_LABEL_HEIGHT ? el : null;
|
|
28
|
+
};
|
|
29
|
+
const anchor =
|
|
30
|
+
asLabel(field.previousElementSibling) ||
|
|
31
|
+
asLabel(field.parentElement && field.parentElement.previousElementSibling) ||
|
|
32
|
+
field;
|
|
33
|
+
// No sticky-header reservation here: every current caller (NewMainPage '/', FilterContent on
|
|
34
|
+
// '/search*') sits on a page where TopBar's own autoHideOnScroll is on (App.tsx: isHomePage ||
|
|
35
|
+
// isSearchPage) — the header slides itself out of the way the instant this scroll starts,
|
|
36
|
+
// independently of this call, so reserving space for it here would double-count.
|
|
37
|
+
const GAP = 8;
|
|
38
|
+
const rect = anchor.getBoundingClientRect();
|
|
39
|
+
const targetY = window.scrollY + rect.top - GAP;
|
|
40
|
+
window.scrollTo({ top: Math.max(0, targetY), behavior: 'smooth' });
|
|
41
|
+
}
|
|
@@ -79,6 +79,20 @@ const ESTIMATED_MENU_HEIGHT = 300;
|
|
|
79
79
|
// a scroll within this window of opening REPOSITIONS instead of closing — see the effect below.
|
|
80
80
|
const SELF_SCROLL_GRACE_MS = 500;
|
|
81
81
|
|
|
82
|
+
// Module-level (not hook-local) so Dropdown.tsx's own searchable-open scroll effect can reuse the
|
|
83
|
+
// exact same measurement — the sticky app header (.topbar, position:sticky/top:0) sits over the
|
|
84
|
+
// top of the viewport on every page that uses it (TopBar's autoHideOnScroll is opt-in, off by
|
|
85
|
+
// default — most pages keep it permanently docked), so anything that scrolls/positions content
|
|
86
|
+
// near the top of the viewport needs to stop at its live bottom edge, not y=0. Reading the
|
|
87
|
+
// header's live bottom edge (0 when absent, e.g. inside a modal, or when autoHideOnScroll has slid
|
|
88
|
+
// it away) gives a correct boundary without hardcoding a pixel height that could drift from the
|
|
89
|
+
// header's own CSS.
|
|
90
|
+
export const getSafeTop = (): number => {
|
|
91
|
+
if (typeof document === 'undefined') return 0;
|
|
92
|
+
const header = document.querySelector('.topbar');
|
|
93
|
+
return header ? Math.max(0, header.getBoundingClientRect().bottom) : 0;
|
|
94
|
+
};
|
|
95
|
+
|
|
82
96
|
export function usePortalMenuPosition<TMenuEl extends HTMLElement = HTMLDivElement>({
|
|
83
97
|
enabled,
|
|
84
98
|
open,
|
|
@@ -121,18 +135,6 @@ export function usePortalMenuPosition<TMenuEl extends HTMLElement = HTMLDivEleme
|
|
|
121
135
|
};
|
|
122
136
|
}, [enabled, open, portalRoot]);
|
|
123
137
|
|
|
124
|
-
// the portal menu's z-index (1400, .mselect-menu--portal) is intentionally higher than every
|
|
125
|
-
// other fixed overlay it must paint over (checkout sheets/modals) — which also means it would
|
|
126
|
-
// paint over the sticky app header (.topbar, position:sticky/top:0/z-index:50) if left
|
|
127
|
-
// unconstrained. Reading the header's live bottom edge (0 when absent, e.g. inside a modal, or
|
|
128
|
-
// when TopBar's autoHideOnScroll has slid it away) gives a correct top boundary without
|
|
129
|
-
// hardcoding a pixel height that could drift from the header's own CSS.
|
|
130
|
-
const getSafeTop = (): number => {
|
|
131
|
-
if (typeof document === 'undefined') return 0;
|
|
132
|
-
const header = document.querySelector('.topbar');
|
|
133
|
-
return header ? Math.max(0, header.getBoundingClientRect().bottom) : 0;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
138
|
const [menuPos, setMenuPos] = React.useState<PortalMenuPos | null>(null);
|
|
137
139
|
|
|
138
140
|
const positionMenu = React.useCallback(() => {
|