@bytebrand/fe-ui-core-autobahn 1.0.126 → 1.0.129

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.
@@ -1,191 +1,191 @@
1
- import React, { ReactNode } from 'react';
2
- import styles from './Checkbox.styl';
3
- import classnames from 'classnames';
4
- import IconSVG from '../../iconsvg/IconSVG';
5
- import Tooltip from '../Tooltip/Tooltip';
6
- import FormattedNumber from '../FormattedNumber/FormattedNumber';
7
-
8
- export interface ICheckboxProps {
9
- // Native checkbox props
10
- name?: string;
11
- value?: any;
12
- checked?: boolean;
13
- disabled?: boolean;
14
- reversed?: boolean;
15
- isAboAccordionExpanded0?: boolean;
16
- onChange?: (event: React.ChangeEvent<HTMLInputElement>, checked?: boolean, value?: any) => void;
17
- onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
18
- onFocus?: (event: React.FocusEvent<HTMLDivElement>) => void;
19
-
20
- // Custom checkbox props
21
- containerClassName?: string;
22
- checkboxClassName?: string;
23
- labelClassName?: string;
24
- labelContentClassName?: string;
25
- tooltipDescription?: string;
26
- checkboxPriceValue: any;
27
- tooltipPosX?: string;
28
- error?: boolean;
29
- label?: JSX.Element | string | null | ReactNode;
30
- }
31
-
32
- // Unique-per-instance id for the text content span, so the (visually hidden) native
33
- // <input type="checkbox"> can point aria-labelledby at it — see render() below for why the
34
- // text moved out of the <label> and needs this to still work as the checkbox's accessible
35
- // name for screen readers.
36
- let contentIdCounter = 0;
37
-
38
- class Checkbox extends React.Component<ICheckboxProps, {}> {
39
- static defaultProps = {
40
- name: '',
41
- value: '',
42
- checked: false,
43
- disabled: false,
44
- reversed: false,
45
-
46
- onChange: () => { },
47
- onBlur: () => { },
48
- onFocus: () => { },
49
-
50
- containerClassName: '',
51
- checkboxClassName: '',
52
- labelClassName: '',
53
- labelContentClassName: '',
54
- tooltipDescription: '',
55
- checkboxPriceValue: '',
56
- tooltipPosX: '',
57
-
58
- error: false,
59
- label: false
60
- };
61
-
62
- input: HTMLInputElement = null;
63
- contentId: string;
64
-
65
- constructor(props: ICheckboxProps) {
66
- super(props);
67
- contentIdCounter += 1;
68
- this.contentId = `checkbox-content-${contentIdCounter}`;
69
- }
70
-
71
- setInputRef = (ref: HTMLInputElement) => {
72
- this.input = ref;
73
- };
74
-
75
- onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
76
- const { disabled, value } = this.props;
77
- const { checked } = event.target;
78
-
79
- if (disabled) {
80
- return false;
81
- }
82
- if (!!event) {
83
- event.stopPropagation();
84
- event.nativeEvent.stopImmediatePropagation();
85
- }
86
-
87
- this.props.onChange(event, checked, value);
88
- };
89
-
90
- onKeyPress = (event: any) => {
91
- const { disabled } = this.props;
92
- const checked = !this.input.checked;
93
- const value = this.input.value;
94
-
95
- if (disabled) {
96
- return false;
97
- }
98
-
99
- this.props.onChange(event, checked, value);
100
- };
101
-
102
- render() {
103
- const {
104
- containerClassName, checkboxClassName, labelClassName, labelContentClassName, reversed,
105
- checked, disabled, value, error, label, tooltipDescription, checkboxPriceValue, tooltipPosX, isAboAccordionExpanded0 = true
106
- } = this.props;
107
-
108
- const containerClasses = classnames(
109
- styles.container,
110
- containerClassName,
111
- { [styles.checked]: checked },
112
- { [styles.error]: error },
113
- { [styles.disabled]: disabled }
114
- );
115
-
116
- const labelClasses = classnames(
117
- styles.label,
118
- labelClassName
119
- );
120
-
121
- const labelContentClasses = classnames(
122
- styles.labelContent,
123
- labelContentClassName
124
- );
125
-
126
- const checkboxClasses = classnames(
127
- styles.checkbox,
128
- checkboxClassName,
129
- { [styles.withLabel]: !!label }
130
- );
131
-
132
- const wrapInputClasses = classnames(
133
- styles.wrapInput,
134
- { [styles.reversed]: reversed }
135
- );
136
-
137
- return (
138
- // tabIndex: was 1 (a positive value) — a well-known anti-pattern (WCAG 2.4.3):
139
- // positive tabindex elements are visited in their own ascending-order pass BEFORE
140
- // every tabindex="0"/naturally-focusable element on the page, regardless of where
141
- // they sit in the DOM. In BaseModal's new focus trap (which walks the panel's
142
- // focusable elements in DOM order to decide where Tab should wrap) that mismatch
143
- // made this checkbox unreachable by Tab entirely inside a trapped modal — verified
144
- // live: 0 hits in 20 Tab presses cycling the RegistrationForm consent checkbox.
145
- // 0 makes it a normal stop in DOM-order tab flow, matching the trap's own model and
146
- // putting it where a sighted user would visually expect it (after the fields above,
147
- // before the CTA below) instead of unpredictably early.
148
- <div className={containerClasses} tabIndex={0} onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyPress={this.onKeyPress}>
149
- <div className={labelClasses}>
150
- {/* Real <label>, scoped to JUST the checkbox (+ its optional price badge) — was
151
- wrapping the text content too, so clicking anywhere in a long paragraph like the
152
- checkout modal's terms-agreement text toggled the checkbox as a side effect.
153
- Native label-click-forwarding only fires for clicks landing inside THIS element
154
- now, and it still works with zero extra JS since the hidden <input> is nested
155
- inside it. */}
156
- <label className={wrapInputClasses}>
157
- {/* Visual representation of the checkbox */}
158
- {/* HidePlus class for OrderOverview component */}
159
- {checkboxPriceValue && <span className={styles.checkboxPrice}><span className={styles.hidePlus}>+</span><FormattedNumber numbersAfterDot={0} value={checkboxPriceValue} /> €</span>}
160
- {/* Hidden input for SEO and HTML semantics */}
161
- <input
162
- className={styles.hiddenInput}
163
- type='checkbox'
164
- checked={checked}
165
- value={value}
166
- disabled={disabled}
167
- tabIndex={-1}
168
- onChange={this.onChange}
169
- ref={this.setInputRef}
170
- // The text is no longer a DOM ancestor/descendant of this input (moved out to a
171
- // plain sibling <span> below so it stops being a click target) — aria-labelledby
172
- // keeps it as the input's accessible name for assistive tech regardless.
173
- aria-labelledby={label ? this.contentId : undefined}
174
- />
175
-
176
- {isAboAccordionExpanded0 && <span className={checkboxClasses}>{checked ? <IconSVG name='common_CheckBlue' className={styles.checkBlueIcon} /> : false}</span>}
177
-
178
- </label>
179
- {!!label ? (
180
- <span id={this.contentId} className={labelContentClasses}>
181
- {label}
182
- {tooltipDescription && <Tooltip description={tooltipDescription} positionY={'top'} positionX={tooltipPosX} tooltipClassName={styles.tooltipCheckbox} iconName='InfoTransparent' />}
183
- </span>
184
- ) : false}
185
- </div>
186
- </div>
187
- );
188
- }
189
- }
190
-
191
- export default Checkbox;
1
+ import React, { ReactNode } from 'react';
2
+ import styles from './Checkbox.styl';
3
+ import classnames from 'classnames';
4
+ import IconSVG from '../../iconsvg/IconSVG';
5
+ import Tooltip from '../Tooltip/Tooltip';
6
+ import FormattedNumber from '../FormattedNumber/FormattedNumber';
7
+
8
+ export interface ICheckboxProps {
9
+ // Native checkbox props
10
+ name?: string;
11
+ value?: any;
12
+ checked?: boolean;
13
+ disabled?: boolean;
14
+ reversed?: boolean;
15
+ isAboAccordionExpanded0?: boolean;
16
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>, checked?: boolean, value?: any) => void;
17
+ onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
18
+ onFocus?: (event: React.FocusEvent<HTMLDivElement>) => void;
19
+
20
+ // Custom checkbox props
21
+ containerClassName?: string;
22
+ checkboxClassName?: string;
23
+ labelClassName?: string;
24
+ labelContentClassName?: string;
25
+ tooltipDescription?: string;
26
+ checkboxPriceValue: any;
27
+ tooltipPosX?: string;
28
+ error?: boolean;
29
+ label?: JSX.Element | string | null | ReactNode;
30
+ }
31
+
32
+ // Unique-per-instance id for the text content span, so the (visually hidden) native
33
+ // <input type="checkbox"> can point aria-labelledby at it — see render() below for why the
34
+ // text moved out of the <label> and needs this to still work as the checkbox's accessible
35
+ // name for screen readers.
36
+ let contentIdCounter = 0;
37
+
38
+ class Checkbox extends React.Component<ICheckboxProps, {}> {
39
+ static defaultProps = {
40
+ name: '',
41
+ value: '',
42
+ checked: false,
43
+ disabled: false,
44
+ reversed: false,
45
+
46
+ onChange: () => { },
47
+ onBlur: () => { },
48
+ onFocus: () => { },
49
+
50
+ containerClassName: '',
51
+ checkboxClassName: '',
52
+ labelClassName: '',
53
+ labelContentClassName: '',
54
+ tooltipDescription: '',
55
+ checkboxPriceValue: '',
56
+ tooltipPosX: '',
57
+
58
+ error: false,
59
+ label: false
60
+ };
61
+
62
+ input: HTMLInputElement = null;
63
+ contentId: string;
64
+
65
+ constructor(props: ICheckboxProps) {
66
+ super(props);
67
+ contentIdCounter += 1;
68
+ this.contentId = `checkbox-content-${contentIdCounter}`;
69
+ }
70
+
71
+ setInputRef = (ref: HTMLInputElement) => {
72
+ this.input = ref;
73
+ };
74
+
75
+ onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
76
+ const { disabled, value } = this.props;
77
+ const { checked } = event.target;
78
+
79
+ if (disabled) {
80
+ return false;
81
+ }
82
+ if (!!event) {
83
+ event.stopPropagation();
84
+ event.nativeEvent.stopImmediatePropagation();
85
+ }
86
+
87
+ this.props.onChange(event, checked, value);
88
+ };
89
+
90
+ onKeyPress = (event: any) => {
91
+ const { disabled } = this.props;
92
+ const checked = !this.input.checked;
93
+ const value = this.input.value;
94
+
95
+ if (disabled) {
96
+ return false;
97
+ }
98
+
99
+ this.props.onChange(event, checked, value);
100
+ };
101
+
102
+ render() {
103
+ const {
104
+ containerClassName, checkboxClassName, labelClassName, labelContentClassName, reversed,
105
+ checked, disabled, value, error, label, tooltipDescription, checkboxPriceValue, tooltipPosX, isAboAccordionExpanded0 = true
106
+ } = this.props;
107
+
108
+ const containerClasses = classnames(
109
+ styles.container,
110
+ containerClassName,
111
+ { [styles.checked]: checked },
112
+ { [styles.error]: error },
113
+ { [styles.disabled]: disabled }
114
+ );
115
+
116
+ const labelClasses = classnames(
117
+ styles.label,
118
+ labelClassName
119
+ );
120
+
121
+ const labelContentClasses = classnames(
122
+ styles.labelContent,
123
+ labelContentClassName
124
+ );
125
+
126
+ const checkboxClasses = classnames(
127
+ styles.checkbox,
128
+ checkboxClassName,
129
+ { [styles.withLabel]: !!label }
130
+ );
131
+
132
+ const wrapInputClasses = classnames(
133
+ styles.wrapInput,
134
+ { [styles.reversed]: reversed }
135
+ );
136
+
137
+ return (
138
+ // tabIndex: was 1 (a positive value) — a well-known anti-pattern (WCAG 2.4.3):
139
+ // positive tabindex elements are visited in their own ascending-order pass BEFORE
140
+ // every tabindex="0"/naturally-focusable element on the page, regardless of where
141
+ // they sit in the DOM. In BaseModal's new focus trap (which walks the panel's
142
+ // focusable elements in DOM order to decide where Tab should wrap) that mismatch
143
+ // made this checkbox unreachable by Tab entirely inside a trapped modal — verified
144
+ // live: 0 hits in 20 Tab presses cycling the RegistrationForm consent checkbox.
145
+ // 0 makes it a normal stop in DOM-order tab flow, matching the trap's own model and
146
+ // putting it where a sighted user would visually expect it (after the fields above,
147
+ // before the CTA below) instead of unpredictably early.
148
+ <div className={containerClasses} tabIndex={0} onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyPress={this.onKeyPress}>
149
+ <div className={labelClasses}>
150
+ {/* Real <label>, scoped to JUST the checkbox (+ its optional price badge) — was
151
+ wrapping the text content too, so clicking anywhere in a long paragraph like the
152
+ checkout modal's terms-agreement text toggled the checkbox as a side effect.
153
+ Native label-click-forwarding only fires for clicks landing inside THIS element
154
+ now, and it still works with zero extra JS since the hidden <input> is nested
155
+ inside it. */}
156
+ <label className={wrapInputClasses}>
157
+ {/* Visual representation of the checkbox */}
158
+ {/* HidePlus class for OrderOverview component */}
159
+ {checkboxPriceValue && <span className={styles.checkboxPrice}><span className={styles.hidePlus}>+</span><FormattedNumber numbersAfterDot={0} value={checkboxPriceValue} /> €</span>}
160
+ {/* Hidden input for SEO and HTML semantics */}
161
+ <input
162
+ className={styles.hiddenInput}
163
+ type='checkbox'
164
+ checked={checked}
165
+ value={value}
166
+ disabled={disabled}
167
+ tabIndex={-1}
168
+ onChange={this.onChange}
169
+ ref={this.setInputRef}
170
+ // The text is no longer a DOM ancestor/descendant of this input (moved out to a
171
+ // plain sibling <span> below so it stops being a click target) — aria-labelledby
172
+ // keeps it as the input's accessible name for assistive tech regardless.
173
+ aria-labelledby={label ? this.contentId : undefined}
174
+ />
175
+
176
+ {isAboAccordionExpanded0 && <span className={checkboxClasses}>{checked ? <IconSVG name='common_CheckBlue' className={styles.checkBlueIcon} /> : false}</span>}
177
+
178
+ </label>
179
+ {!!label ? (
180
+ <span id={this.contentId} className={labelContentClasses}>
181
+ {label}
182
+ {tooltipDescription && <Tooltip description={tooltipDescription} positionY={'top'} positionX={tooltipPosX} tooltipClassName={styles.tooltipCheckbox} iconName='InfoTransparent' />}
183
+ </span>
184
+ ) : false}
185
+ </div>
186
+ </div>
187
+ );
188
+ }
189
+ }
190
+
191
+ export default Checkbox;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.126",
3
+ "version": "1.0.129",
4
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
5
  "main": "index.ts",
6
6
  "license": "UNLICENSED",
package/theme/md3.css CHANGED
@@ -5695,11 +5695,29 @@ line-height: 31px;
5695
5695
  pill, CTA buttons) shifts when .fin-cond collapses to empty on Buy. */
5696
5696
  .md3 .fin-calc-card .fin-price-grid.fin-price-grid--cash {
5697
5697
  gap: 12px 15px;
5698
+ /* Cross-tab height parity (desktop): with no floor at all, Financing/Leasing's .fin-calc-card
5699
+ naturally measures 372×366.19px (their own real content — .fin-rate-main + the "0 €
5700
+ Anzahlung · 60 Monate · ..." .fin-cond line). Kaufen has no .fin-cond, but its .fin-rate-main
5701
+ is itself taller (the "Festpreis inkl. MwSt." caption, +the struck old list price when the
5702
+ car has a discount) — the two roughly cancel out, landing Kaufen within ~1px of the same
5703
+ 366.19px on its own, with nothing reserved here at all. This 1px closes that last gap so all
5704
+ three tabs measure identically. (Earlier passes at 21/27px reserved space Kaufen's .fin-cond
5705
+ row never needed and weren't the actual cause — see .fin-rate-main above.) */
5706
+ grid-template-rows: auto 1px;
5698
5707
  }
5699
5708
  .md3 .fin-rate-main {
5700
5709
  grid-area: rate;
5701
5710
  min-width: 0;
5702
5711
  }
5712
+ /* Kaufen desktop: reserves the struck-old-price row's height (FinanceCalculator.tsx now always
5713
+ renders this wrapper on Kaufen, content or not — see its own comment) so .fin-rate-main is the
5714
+ SAME height whether this car has a discount or not. 20px — measured value (devtools, the <s>
5715
+ price line's own rendered height), confirmed. Without this, a no-discount Kaufen car's whole
5716
+ .fin-calc-card lands ~20px shorter than Financing/Leasing, breaking the cross-tab height parity
5717
+ the .fin-price-grid--cash rule above (1px) assumes. */
5718
+ .md3 .fin-rate-strike-slot {
5719
+ min-height: 20px;
5720
+ }
5703
5721
  /* Kaufen desktop hero price footnote — same small/quiet treatment as the mobile
5704
5722
  .fin-rate-cash-mobile sup, instead of inheriting the 48px blue hero size. */
5705
5723
  .md3 .fin-rate-main .num.hide-mobile sup {
@@ -5736,6 +5754,17 @@ line-height: 31px;
5736
5754
  grid-template-columns: minmax(0, 1fr) 112px;
5737
5755
  grid-template-areas: "rate evalcol" "cond cond";
5738
5756
  column-gap: 15px;
5757
+ /* Same cross-tab jump as mobile (see the ≤600px rule further down this file, "Request/Call row
5758
+ jump"), mirrored for desktop: on Kaufen, .fin-cond isn't rendered at all (isCash — see
5759
+ FinanceCalculator.tsx), collapsing the "cond" row to 0 instead of a text line, which pulls
5760
+ .fin-adjust/the CTA buttons/.fin-info up compared to Financing/Leasing. A min-height on the
5761
+ whole grid was tried first and reverted — align-content defaults to stretch for grid
5762
+ containers, so the reserved space split across BOTH rows instead of just the missing one,
5763
+ opening a big gap under the price on Kaufen. Reserving the row itself (not the container)
5764
+ targets only the row that actually disappears.
5765
+ 21px — measured value (devtools: Leasing 166.19px total − Kaufen 145.19px total), confirmed.
5766
+ (A later bump to 27px, chasing a 6px .fin-calc-card-level residual, was wrong and reverted —
5767
+ that 6px comes from outside .fin-price-grid, see the .fin-adjust rule below.) */
5739
5768
  }
5740
5769
  .md3 .fin-calc-card .fin-eval-col {
5741
5770
  display: flex;