@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.65 → 2.0.0-next.67
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/bundle/openbridge-webcomponents.bundle.js +15992 -15907
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +138 -30
- package/dist/automation/automation-button/automation-button.css.js +9 -9
- package/dist/automation/valve-analoge-two-way-icon/valve-analog-two-way-icon.d.ts +1 -0
- package/dist/automation/valve-analoge-two-way-icon/valve-analog-two-way-icon.d.ts.map +1 -1
- package/dist/automation/valve-analoge-two-way-icon/valve-analog-two-way-icon.js +9 -28
- package/dist/automation/valve-analoge-two-way-icon/valve-analog-two-way-icon.js.map +1 -1
- package/dist/components/accordion-card/accordion-card.css.js +1 -1
- package/dist/components/dropdown-button/dropdown-button.d.ts +11 -0
- package/dist/components/dropdown-button/dropdown-button.d.ts.map +1 -1
- package/dist/components/dropdown-button/dropdown-button.js +24 -2
- package/dist/components/dropdown-button/dropdown-button.js.map +1 -1
- package/dist/components/keyboard-numeric/keyboard-numeric.d.ts +1 -2
- package/dist/components/keyboard-numeric/keyboard-numeric.d.ts.map +1 -1
- package/dist/components/keyboard-numeric/keyboard-numeric.js +10 -15
- package/dist/components/keyboard-numeric/keyboard-numeric.js.map +1 -1
- package/dist/components/number-input-field/number-input-field.d.ts +9 -0
- package/dist/components/number-input-field/number-input-field.d.ts.map +1 -1
- package/dist/components/number-input-field/number-input-field.js +30 -1
- package/dist/components/number-input-field/number-input-field.js.map +1 -1
- package/dist/components/number-input-field/number-input-format.d.ts +9 -0
- package/dist/components/number-input-field/number-input-format.d.ts.map +1 -1
- package/dist/components/number-input-field/number-input-format.js +33 -2
- package/dist/components/number-input-field/number-input-format.js.map +1 -1
- package/dist/components/stepper-box/stepper-box.css.js +0 -6
- package/dist/components/stepper-box/stepper-box.css.js.map +1 -1
- package/dist/components/stepper-box/stepper-box.d.ts +3 -13
- package/dist/components/stepper-box/stepper-box.d.ts.map +1 -1
- package/dist/components/stepper-box/stepper-box.js.map +1 -1
- package/dist/components/toggle-button-group/toggle-button-group.d.ts +14 -0
- package/dist/components/toggle-button-group/toggle-button-group.d.ts.map +1 -1
- package/dist/components/toggle-button-group/toggle-button-group.js +25 -5
- package/dist/components/toggle-button-group/toggle-button-group.js.map +1 -1
- package/dist/integration-systems/integration-fleet-button/integration-fleet-button.css.js +4 -0
- package/dist/integration-systems/integration-fleet-button/integration-fleet-button.css.js.map +1 -1
- package/dist/integration-systems/integration-fleet-button/integration-fleet-button.d.ts +1 -0
- package/dist/integration-systems/integration-fleet-button/integration-fleet-button.d.ts.map +1 -1
- package/dist/integration-systems/integration-fleet-button/integration-fleet-button.js +13 -1
- package/dist/integration-systems/integration-fleet-button/integration-fleet-button.js.map +1 -1
- package/docs/GettingStarted.mdx +28 -12
- package/package.json +1 -1
- package/src/automation/automation-button/automation-button.css +9 -9
- package/src/automation/valve-analoge-two-way-icon/valve-analog-two-way-icon.ts +9 -28
- package/src/components/accordion-card/accordion-card.css +1 -1
- package/src/components/dropdown-button/dropdown-button.stories.ts +22 -0
- package/src/components/dropdown-button/dropdown-button.ts +33 -4
- package/src/components/keyboard-numeric/keyboard-numeric.ts +12 -18
- package/src/components/number-input-field/number-input-field.spec.ts +134 -0
- package/src/components/number-input-field/number-input-field.ts +47 -0
- package/src/components/number-input-field/number-input-format.spec.ts +62 -0
- package/src/components/number-input-field/number-input-format.ts +59 -2
- package/src/components/stepper-box/stepper-box.css +0 -6
- package/src/components/stepper-box/stepper-box.ts +3 -13
- package/src/components/toggle-button-group/toggle-button-group.ts +36 -5
- package/src/components/toggle-button-group/toggle-button.stories.ts +31 -0
- package/src/integration-systems/integration-fleet-button/integration-fleet-button.css +4 -0
- package/src/integration-systems/integration-fleet-button/integration-fleet-button.stories.ts +9 -0
- package/src/integration-systems/integration-fleet-button/integration-fleet-button.ts +10 -1
- package/src/navigation-instruments/readout/readout.stories.ts +0 -6
|
@@ -2,6 +2,7 @@ import {LitElement, html, svg, unsafeCSS} from 'lit';
|
|
|
2
2
|
import {property} from 'lit/decorators.js';
|
|
3
3
|
import compentStyle from './valve-analog-two-way-icon.css?inline';
|
|
4
4
|
import {customElement} from '../../decorator.js';
|
|
5
|
+
import '../../icons/icon-twoway-analog-closed.js';
|
|
5
6
|
|
|
6
7
|
@customElement('obc-valve-analog-two-way-icon')
|
|
7
8
|
export class ObcValveAnalogTwoWayIcon extends LitElement {
|
|
@@ -13,36 +14,16 @@ export class ObcValveAnalogTwoWayIcon extends LitElement {
|
|
|
13
14
|
const transform = this.vertical ? 'transform: rotate(90deg);' : '';
|
|
14
15
|
if (this.closed) {
|
|
15
16
|
return html` <div class="wrapper" style="${transform}">
|
|
16
|
-
<
|
|
17
|
-
width="24"
|
|
18
|
-
height="24"
|
|
19
|
-
viewBox="0 0 24 24"
|
|
20
|
-
fill="none"
|
|
21
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
22
|
-
>
|
|
23
|
-
<path
|
|
24
|
-
fill-rule="evenodd"
|
|
25
|
-
clip-rule="evenodd"
|
|
26
|
-
d="M3 16.8892L10 12.8059V10.4546L3 6.37127V16.8892ZM11 9.88024V13.3802L2.75194 18.1916C2.41861 18.386 2 18.1456 2 17.7597V5.50075C2 5.11486 2.41861 4.87442 2.75193 5.06886L11 9.88024ZM21 16.8892V6.37127L14 10.4546V12.8059L21 16.8892ZM13 13.3802L21.2481 18.1916C21.5814 18.386 22 18.1456 22 17.7597V5.50075C22 5.11486 21.5814 4.87442 21.2481 5.06886L13 9.88024V13.3802ZM12 22.6301C11.4477 22.6301 11 22.1824 11 21.6301V15.6301C11 15.0778 11.4477 14.6301 12 14.6301C12.5523 14.6301 13 15.0778 13 15.6301V21.6301C13 22.1824 12.5523 22.6301 12 22.6301Z"
|
|
27
|
-
fill="var(--automation-device-secondary-color)"
|
|
28
|
-
/>
|
|
29
|
-
<path
|
|
30
|
-
d="M10 12.8059L3 16.8892V6.37127L10 10.4546V12.8059Z"
|
|
31
|
-
fill="var(--automation-device-tertiary-color)"
|
|
32
|
-
/>
|
|
33
|
-
<path
|
|
34
|
-
d="M21 6.37127V16.8892L14 12.8059V10.4546L21 6.37127Z"
|
|
35
|
-
fill="var(--automation-device-tertiary-color)"
|
|
36
|
-
/>
|
|
37
|
-
</svg>
|
|
17
|
+
<obi-twoway-analog-closed useCssColor> </obi-twoway-analog-closed>
|
|
38
18
|
</div>`;
|
|
39
19
|
}
|
|
40
20
|
|
|
41
21
|
const handleRotation = -(1 - this.value / 100) * 90;
|
|
22
|
+
const handleTranslation = (1 - this.value / 100) * 2;
|
|
42
23
|
const handle = svg`
|
|
43
|
-
<g transform="rotate(${handleRotation} 12
|
|
44
|
-
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.
|
|
45
|
-
<path d="
|
|
24
|
+
<g transform="translate(0, ${handleTranslation}) rotate(${handleRotation} 12 3.5) ">
|
|
25
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 5H14.5C15.3284 5 16 4.32843 16 3.5C16 2.67157 15.3284 2 14.5 2H9.5C8.67157 2 8 2.67157 8 3.5C8 4.32843 8.67157 5 9.5 5ZM9.5 4L14.5 4C14.7761 4 15 3.77614 15 3.5C15 3.22386 14.7761 3 14.5 3L9.5 3C9.22386 3 9 3.22386 9 3.5C9 3.77614 9.22386 4 9.5 4Z" fill="var(--automation-device-tertiary-color)"/>
|
|
26
|
+
<path d="M9.5 4L14.5 4C14.7761 4 15 3.77614 15 3.5C15 3.22386 14.7761 3 14.5 3L9.5 3C9.22386 3 9 3.22386 9 3.5C9 3.77614 9.22386 4 9.5 4Z" fill="var(--automation-device-primary-color)"/>
|
|
46
27
|
</g>
|
|
47
28
|
`;
|
|
48
29
|
|
|
@@ -60,11 +41,11 @@ export class ObcValveAnalogTwoWayIcon extends LitElement {
|
|
|
60
41
|
xmlns="http://www.w3.org/2000/svg"
|
|
61
42
|
>
|
|
62
43
|
<path
|
|
63
|
-
d="
|
|
44
|
+
d="M11 11L3.5547 6.03645C2.89015 5.59342 2 6.06981 2 6.8685V19.1315C2 19.9302 2.89015 20.4066 3.5547 19.9635L11 15H13L20.4453 19.9635C21.1099 20.4066 22 19.9302 22 19.1315V6.8685C22 6.06981 21.1099 5.59342 20.4453 6.03645L13 11H11ZM3 6.8685L3 19.1315L10.6972 14H13.3028L21 19.1315V6.8685L13.3028 12H10.6972L3 6.8685Z"
|
|
64
45
|
fill="var(--automation-device-tertiary-color)"
|
|
65
46
|
/>
|
|
66
47
|
<path
|
|
67
|
-
d="
|
|
48
|
+
d="M10.6972 12H13.3028L21 6.8685V19.1315L13.3028 14H10.6972L3 19.1315V6.8685L10.6972 12Z"
|
|
68
49
|
fill="var(--automation-device-primary-color)"
|
|
69
50
|
/>
|
|
70
51
|
${handle}
|
|
@@ -104,7 +85,7 @@ export class ObcValveAnalogTwoWayIcon extends LitElement {
|
|
|
104
85
|
<defs>
|
|
105
86
|
<clipPath id="clip0">
|
|
106
87
|
<path
|
|
107
|
-
d="
|
|
88
|
+
d="M10.6972 12H13.3028L21 6.8685V19.1315L13.3028 14H10.6972L3 19.1315V6.8685L10.6972 12Z"
|
|
108
89
|
/>
|
|
109
90
|
</clipPath>
|
|
110
91
|
</defs>
|
|
@@ -25,6 +25,12 @@ const meta: Meta<ObcDropdownButton> = {
|
|
|
25
25
|
openTop: {
|
|
26
26
|
control: 'boolean',
|
|
27
27
|
},
|
|
28
|
+
allowEmptySelection: {
|
|
29
|
+
control: 'boolean',
|
|
30
|
+
},
|
|
31
|
+
placeholder: {
|
|
32
|
+
control: 'text',
|
|
33
|
+
},
|
|
28
34
|
},
|
|
29
35
|
args: {
|
|
30
36
|
options: [
|
|
@@ -171,3 +177,19 @@ export const DisabledWithIcon: Story = {
|
|
|
171
177
|
</obc-dropdown-button>
|
|
172
178
|
`,
|
|
173
179
|
};
|
|
180
|
+
|
|
181
|
+
export const EmptySelection: Story = {
|
|
182
|
+
args: {
|
|
183
|
+
value: 'none',
|
|
184
|
+
allowEmptySelection: true,
|
|
185
|
+
placeholder: 'Select a brand',
|
|
186
|
+
},
|
|
187
|
+
render: (args) => html`
|
|
188
|
+
<obc-dropdown-button
|
|
189
|
+
.options=${args.options}
|
|
190
|
+
.value=${args.value}
|
|
191
|
+
.allowEmptySelection=${args.allowEmptySelection}
|
|
192
|
+
.placeholder=${args.placeholder}
|
|
193
|
+
></obc-dropdown-button>
|
|
194
|
+
`,
|
|
195
|
+
};
|
|
@@ -49,6 +49,8 @@ export type DropdownButtonOption = {
|
|
|
49
49
|
* - `options` (Array): List of selectable options, each with a `value` (string), `label` (string), and optional `level` (number) for indentation.
|
|
50
50
|
* - `value` (string): The currently selected option's value. If not set, defaults to the first option.
|
|
51
51
|
* - `fullWidth` (boolean): Expands the component to fill its container when true. Default is false.
|
|
52
|
+
* - `allowEmptySelection` (boolean): When true, a `value` that does not match any option leaves the button with no option selected, showing `placeholder` instead of defaulting to the first option. Default is false.
|
|
53
|
+
* - `placeholder` (string): Text shown when nothing is selected and `allowEmptySelection` is true. Default is an empty string.
|
|
52
54
|
*
|
|
53
55
|
* ### Events
|
|
54
56
|
* - `dropdown-change` – Fired when the user selects a different option. The event detail includes `{ value, label }` of the selected option.
|
|
@@ -102,6 +104,17 @@ export class ObcDropdownButton extends LitElement {
|
|
|
102
104
|
*/
|
|
103
105
|
@property({type: Boolean}) fullWidth = false;
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* If true, a `value` that does not match any option leaves the button with no option selected,
|
|
109
|
+
* showing `placeholder` instead of defaulting to the first option. Default is false.
|
|
110
|
+
*/
|
|
111
|
+
@property({type: Boolean}) allowEmptySelection = false;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Text shown when nothing is selected and `allowEmptySelection` is true. Default is an empty string.
|
|
115
|
+
*/
|
|
116
|
+
@property({type: String}) placeholder = '';
|
|
117
|
+
|
|
105
118
|
/**
|
|
106
119
|
* Controls the button's display type.
|
|
107
120
|
* - `label`: Text label only (default)
|
|
@@ -142,10 +155,21 @@ export class ObcDropdownButton extends LitElement {
|
|
|
142
155
|
this.selectedLabel = '';
|
|
143
156
|
return;
|
|
144
157
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
const match = this.value
|
|
159
|
+
? this.options.find((item) => item.value === this.value)
|
|
160
|
+
: undefined;
|
|
161
|
+
if (match) {
|
|
162
|
+
this.selectedValue = match.value;
|
|
163
|
+
this.selectedLabel = match.label;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this.allowEmptySelection) {
|
|
167
|
+
this.selectedValue = '';
|
|
168
|
+
this.selectedLabel = this.placeholder;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.selectedValue = this.options[0].value;
|
|
172
|
+
this.selectedLabel = this.options[0].label;
|
|
149
173
|
}
|
|
150
174
|
|
|
151
175
|
override render() {
|
|
@@ -172,6 +196,11 @@ export class ObcDropdownButton extends LitElement {
|
|
|
172
196
|
</div>
|
|
173
197
|
</div>
|
|
174
198
|
<select @change=${this.changeHandler} ?disabled=${this.disabled}>
|
|
199
|
+
${this.allowEmptySelection && this.selectedValue === ''
|
|
200
|
+
? html`<option value="" disabled selected hidden>
|
|
201
|
+
${this.placeholder}
|
|
202
|
+
</option>`
|
|
203
|
+
: nothing}
|
|
175
204
|
${this.options.map((item) => {
|
|
176
205
|
const indent = item.level ? (item.level - 1) * 2 : 0;
|
|
177
206
|
const indentText = [];
|
|
@@ -79,6 +79,10 @@ const ALLOWED_CHARS = [
|
|
|
79
79
|
|
|
80
80
|
const OPERATORS = ['+', '-', '–', '×', '÷', '*', '/'];
|
|
81
81
|
|
|
82
|
+
const ALLOWED_CHARS_PATTERN = `^[${ALLOWED_CHARS.map((c) =>
|
|
83
|
+
c.replace(/[-.*+?^${}()|[\]\\]/g, '\\$&')
|
|
84
|
+
).join('')}]*$`;
|
|
85
|
+
|
|
82
86
|
@customElement('obc-keyboard-numeric')
|
|
83
87
|
export class ObcKeyboardNumeric extends LitElement {
|
|
84
88
|
@property({type: String}) type: ObcKeyboardNumericType =
|
|
@@ -118,6 +122,13 @@ export class ObcKeyboardNumeric extends LitElement {
|
|
|
118
122
|
@state() private content: ObcKeyboardNumericContent =
|
|
119
123
|
ObcKeyboardNumericContent.Numbers;
|
|
120
124
|
|
|
125
|
+
private get effectiveValidationPattern(): string {
|
|
126
|
+
if (!this.validationPattern) {
|
|
127
|
+
return ALLOWED_CHARS_PATTERN;
|
|
128
|
+
}
|
|
129
|
+
return `^(?=${ALLOWED_CHARS_PATTERN})(?=${this.validationPattern}).*$`;
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
/** Validates if a character can be added to the current value */
|
|
122
133
|
private canAddCharacter(char: string): boolean {
|
|
123
134
|
const potentialValue = this.value + char;
|
|
@@ -150,11 +161,6 @@ export class ObcKeyboardNumeric extends LitElement {
|
|
|
150
161
|
return true;
|
|
151
162
|
}
|
|
152
163
|
|
|
153
|
-
/** Validates that all characters in a value are allowed */
|
|
154
|
-
private isValidValue(value: string): boolean {
|
|
155
|
-
return [...value].every((char) => ALLOWED_CHARS.includes(char));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
164
|
private handleCloseClick() {
|
|
159
165
|
this.dispatchEvent(
|
|
160
166
|
new CustomEvent('close-click', {bubbles: true, composed: true})
|
|
@@ -254,19 +260,6 @@ export class ObcKeyboardNumeric extends LitElement {
|
|
|
254
260
|
const input = e.target as ObcNumberInputField;
|
|
255
261
|
const newValue = input.displayValue;
|
|
256
262
|
|
|
257
|
-
if (this.validationPattern && newValue) {
|
|
258
|
-
const regex = new RegExp(this.validationPattern);
|
|
259
|
-
if (!regex.test(newValue)) {
|
|
260
|
-
input.displayOverride = this.value;
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (newValue && !this.isValidValue(newValue)) {
|
|
266
|
-
input.displayOverride = this.value;
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
263
|
this.value = newValue;
|
|
271
264
|
this.dispatchValueChange();
|
|
272
265
|
}
|
|
@@ -359,6 +352,7 @@ export class ObcKeyboardNumeric extends LitElement {
|
|
|
359
352
|
.textAlign=${this.inputFieldTextAlign}
|
|
360
353
|
.size=${ObcNumberInputFieldSize.Large}
|
|
361
354
|
.helperText=${this.helperText}
|
|
355
|
+
.validationPattern=${this.effectiveValidationPattern}
|
|
362
356
|
?hasLeadingIcon=${this.hasLeadingIcon}
|
|
363
357
|
placeholder="00.0"
|
|
364
358
|
@input=${this.handleInput}
|
|
@@ -218,4 +218,138 @@ describe('obc-number-input-field', () => {
|
|
|
218
218
|
expect(input.value).toBe('10.');
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
describe('beforeinput filtering', () => {
|
|
223
|
+
function dispatchBeforeInput(
|
|
224
|
+
target: HTMLInputElement,
|
|
225
|
+
data: string | null,
|
|
226
|
+
inputType = 'insertText'
|
|
227
|
+
): InputEvent {
|
|
228
|
+
const event = new InputEvent('beforeinput', {
|
|
229
|
+
data,
|
|
230
|
+
inputType,
|
|
231
|
+
cancelable: true,
|
|
232
|
+
bubbles: true,
|
|
233
|
+
});
|
|
234
|
+
target.dispatchEvent(event);
|
|
235
|
+
return event;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
beforeEach(async () => {
|
|
239
|
+
el.decimalSeparator = '.';
|
|
240
|
+
el.groupSeparator = ',';
|
|
241
|
+
input.focus();
|
|
242
|
+
input.value = '';
|
|
243
|
+
await el.updateComplete;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('blocks letters', () => {
|
|
247
|
+
const event = dispatchBeforeInput(input, 'a');
|
|
248
|
+
expect(event.defaultPrevented).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('blocks scientific notation', () => {
|
|
252
|
+
input.value = '1';
|
|
253
|
+
input.setSelectionRange(1, 1);
|
|
254
|
+
const event = dispatchBeforeInput(input, 'e');
|
|
255
|
+
expect(event.defaultPrevented).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('blocks symbols', () => {
|
|
259
|
+
const event = dispatchBeforeInput(input, '!');
|
|
260
|
+
expect(event.defaultPrevented).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('allows digits', () => {
|
|
264
|
+
const event = dispatchBeforeInput(input, '1');
|
|
265
|
+
expect(event.defaultPrevented).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('allows decimal separator', () => {
|
|
269
|
+
const event = dispatchBeforeInput(input, '.');
|
|
270
|
+
expect(event.defaultPrevented).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('allows comma when locale uses it', () => {
|
|
274
|
+
const event = dispatchBeforeInput(input, ',');
|
|
275
|
+
expect(event.defaultPrevented).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('allows leading sign', () => {
|
|
279
|
+
const event = dispatchBeforeInput(input, '-');
|
|
280
|
+
expect(event.defaultPrevented).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('allows deletions (null data)', () => {
|
|
284
|
+
input.value = '12';
|
|
285
|
+
input.setSelectionRange(2, 2);
|
|
286
|
+
const event = dispatchBeforeInput(input, null, 'deleteContentBackward');
|
|
287
|
+
expect(event.defaultPrevented).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('blocks paste containing letters', () => {
|
|
291
|
+
const event = new InputEvent('beforeinput', {
|
|
292
|
+
data: null,
|
|
293
|
+
inputType: 'insertFromPaste',
|
|
294
|
+
cancelable: true,
|
|
295
|
+
bubbles: true,
|
|
296
|
+
});
|
|
297
|
+
const dt = new DataTransfer();
|
|
298
|
+
dt.setData('text/plain', '12abc34');
|
|
299
|
+
Object.defineProperty(event, 'dataTransfer', {value: dt});
|
|
300
|
+
input.dispatchEvent(event);
|
|
301
|
+
expect(event.defaultPrevented).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('allows paste of numeric string with separators', () => {
|
|
305
|
+
const event = new InputEvent('beforeinput', {
|
|
306
|
+
data: null,
|
|
307
|
+
inputType: 'insertFromPaste',
|
|
308
|
+
cancelable: true,
|
|
309
|
+
bubbles: true,
|
|
310
|
+
});
|
|
311
|
+
const dt = new DataTransfer();
|
|
312
|
+
dt.setData('text/plain', '1,234.5');
|
|
313
|
+
Object.defineProperty(event, 'dataTransfer', {value: dt});
|
|
314
|
+
input.dispatchEvent(event);
|
|
315
|
+
expect(event.defaultPrevented).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('honours custom decimalSeparator', async () => {
|
|
319
|
+
el.decimalSeparator = ',';
|
|
320
|
+
el.groupSeparator = ' ';
|
|
321
|
+
await el.updateComplete;
|
|
322
|
+
|
|
323
|
+
const dot = dispatchBeforeInput(input, '.');
|
|
324
|
+
expect(dot.defaultPrevented).toBe(true);
|
|
325
|
+
|
|
326
|
+
const comma = dispatchBeforeInput(input, ',');
|
|
327
|
+
expect(comma.defaultPrevented).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('honours custom validationPattern', async () => {
|
|
331
|
+
el.validationPattern = '^[0-9]*$';
|
|
332
|
+
await el.updateComplete;
|
|
333
|
+
|
|
334
|
+
const minus = dispatchBeforeInput(input, '-');
|
|
335
|
+
expect(minus.defaultPrevented).toBe(true);
|
|
336
|
+
|
|
337
|
+
const digit = dispatchBeforeInput(input, '1');
|
|
338
|
+
expect(digit.defaultPrevented).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('does not block when disabled', async () => {
|
|
342
|
+
el.disabled = true;
|
|
343
|
+
await el.updateComplete;
|
|
344
|
+
const event = dispatchBeforeInput(input, 'a');
|
|
345
|
+
expect(event.defaultPrevented).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('does not block when readonly', async () => {
|
|
349
|
+
el.readonly = true;
|
|
350
|
+
await el.updateComplete;
|
|
351
|
+
const event = dispatchBeforeInput(input, 'a');
|
|
352
|
+
expect(event.defaultPrevented).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
221
355
|
});
|
|
@@ -13,6 +13,7 @@ import {classMap} from 'lit/directives/class-map.js';
|
|
|
13
13
|
import {customElement} from '../../decorator.js';
|
|
14
14
|
import {
|
|
15
15
|
formatNumberForDisplay,
|
|
16
|
+
isAllowedIntermediateInput,
|
|
16
17
|
NumberInputFormatOptions,
|
|
17
18
|
parseNumberInput,
|
|
18
19
|
removeGroupingFromDisplay,
|
|
@@ -113,6 +114,14 @@ export class ObcNumberInputField extends LitElement {
|
|
|
113
114
|
@property({type: Number}) minFractionDigits = 0;
|
|
114
115
|
@property({type: Number}) maxFractionDigits?: number | undefined;
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Optional regex pattern. When set, any keystroke or paste whose resulting
|
|
119
|
+
* value does not match this pattern is blocked at the `beforeinput` stage.
|
|
120
|
+
* When unset, a permissive numeric filter (digits, sign, active separators,
|
|
121
|
+
* whitespace) is applied instead.
|
|
122
|
+
*/
|
|
123
|
+
@property({type: String}) validationPattern = '';
|
|
124
|
+
|
|
116
125
|
@state() private hasFocus = false;
|
|
117
126
|
@state() private displayText = '';
|
|
118
127
|
@state() private previousValue = NaN;
|
|
@@ -150,6 +159,43 @@ export class ObcNumberInputField extends LitElement {
|
|
|
150
159
|
this.dispatchInput();
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
private isProjectedValueAllowed(projected: string): boolean {
|
|
163
|
+
if (this.validationPattern) {
|
|
164
|
+
let regex: RegExp;
|
|
165
|
+
try {
|
|
166
|
+
// Anchor the consumer pattern so it must match the whole projected
|
|
167
|
+
// value rather than any substring (e.g. `[0-9]` must not pass `a1`).
|
|
168
|
+
regex = new RegExp(`^(?:${this.validationPattern})$`);
|
|
169
|
+
} catch {
|
|
170
|
+
// Invalid pattern: fall back to the numeric filter instead of
|
|
171
|
+
// allowing arbitrary input.
|
|
172
|
+
return isAllowedIntermediateInput(projected, this.getFormatOptions());
|
|
173
|
+
}
|
|
174
|
+
return regex.test(projected);
|
|
175
|
+
}
|
|
176
|
+
return isAllowedIntermediateInput(projected, this.getFormatOptions());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private onBeforeInput(e: InputEvent) {
|
|
180
|
+
if (this.disabled || this.readonly) return;
|
|
181
|
+
const input = e.target as HTMLInputElement;
|
|
182
|
+
|
|
183
|
+
const incoming = e.data ?? e.dataTransfer?.getData('text/plain') ?? null;
|
|
184
|
+
// Deletions, history, and formatting (`insertLineBreak`, `historyUndo`,
|
|
185
|
+
// `deleteContentBackward`, etc.) have `data === null` and no transferable
|
|
186
|
+
// text — always allow them.
|
|
187
|
+
if (incoming === null) return;
|
|
188
|
+
|
|
189
|
+
const start = input.selectionStart ?? input.value.length;
|
|
190
|
+
const end = input.selectionEnd ?? input.value.length;
|
|
191
|
+
const projected =
|
|
192
|
+
input.value.slice(0, start) + incoming + input.value.slice(end);
|
|
193
|
+
|
|
194
|
+
if (!this.isProjectedValueAllowed(projected)) {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
153
199
|
private onFocus() {
|
|
154
200
|
this.hasFocus = true;
|
|
155
201
|
const source = this.displayOverride || this.displayText;
|
|
@@ -444,6 +490,7 @@ export class ObcNumberInputField extends LitElement {
|
|
|
444
490
|
hasHelperOrError ? 'helper-text' : undefined
|
|
445
491
|
)}
|
|
446
492
|
autocomplete="off"
|
|
493
|
+
@beforeinput=${this.onBeforeInput}
|
|
447
494
|
@input=${this.onInput}
|
|
448
495
|
/>
|
|
449
496
|
${unitInside
|
|
@@ -2,6 +2,7 @@ import {describe, it, expect} from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
formatNumberForDisplay,
|
|
4
4
|
getLocaleNumberSeparators,
|
|
5
|
+
isAllowedIntermediateInput,
|
|
5
6
|
parseNumberInput,
|
|
6
7
|
removeGroupingFromDisplay,
|
|
7
8
|
valuesEqual,
|
|
@@ -22,6 +23,17 @@ describe('parseNumberInput', () => {
|
|
|
22
23
|
expect(parseNumberInput('10,')).toBe(10);
|
|
23
24
|
});
|
|
24
25
|
|
|
26
|
+
it('parses grouped numbers with mixed separators', () => {
|
|
27
|
+
expect(parseNumberInput('1,234.5')).toBe(1234.5);
|
|
28
|
+
expect(parseNumberInput('1.234,5')).toBe(1234.5);
|
|
29
|
+
expect(parseNumberInput('1 234,5')).toBe(1234.5);
|
|
30
|
+
expect(parseNumberInput("1'234.5")).toBe(1234.5);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('parses grouped integers', () => {
|
|
34
|
+
expect(parseNumberInput('1,234,567')).toBe(1234567);
|
|
35
|
+
});
|
|
36
|
+
|
|
25
37
|
it('returns NaN for sign-only input', () => {
|
|
26
38
|
expect(Number.isNaN(parseNumberInput('-'))).toBe(true);
|
|
27
39
|
expect(Number.isNaN(parseNumberInput('+'))).toBe(true);
|
|
@@ -153,3 +165,53 @@ describe('valuesEqual', () => {
|
|
|
153
165
|
expect(valuesEqual(1, 2)).toBe(false);
|
|
154
166
|
});
|
|
155
167
|
});
|
|
168
|
+
|
|
169
|
+
describe('isAllowedIntermediateInput', () => {
|
|
170
|
+
const enUsOptions = {decimalSeparator: '.', groupSeparator: ','};
|
|
171
|
+
|
|
172
|
+
it('allows empty string', () => {
|
|
173
|
+
expect(isAllowedIntermediateInput('', enUsOptions)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('allows digits', () => {
|
|
177
|
+
expect(isAllowedIntermediateInput('123', enUsOptions)).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('allows leading sign', () => {
|
|
181
|
+
expect(isAllowedIntermediateInput('-', enUsOptions)).toBe(true);
|
|
182
|
+
expect(isAllowedIntermediateInput('+12.5', enUsOptions)).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('allows trailing decimal separator', () => {
|
|
186
|
+
expect(isAllowedIntermediateInput('10.', enUsOptions)).toBe(true);
|
|
187
|
+
expect(isAllowedIntermediateInput(',5', enUsOptions)).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('allows grouped numbers with default separators', () => {
|
|
191
|
+
expect(isAllowedIntermediateInput('1,234.5', enUsOptions)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('rejects letters', () => {
|
|
195
|
+
expect(isAllowedIntermediateInput('12a')).toBe(false);
|
|
196
|
+
expect(isAllowedIntermediateInput('e')).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('rejects symbols outside the allowed set', () => {
|
|
200
|
+
expect(isAllowedIntermediateInput('12!')).toBe(false);
|
|
201
|
+
expect(isAllowedIntermediateInput('12$')).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('rejects sign in non-leading position', () => {
|
|
205
|
+
expect(isAllowedIntermediateInput('1-2')).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('honours custom decimal separator', () => {
|
|
209
|
+
const opts = {decimalSeparator: ',', groupSeparator: ' '};
|
|
210
|
+
expect(isAllowedIntermediateInput('1 234,5', opts)).toBe(true);
|
|
211
|
+
expect(isAllowedIntermediateInput('1,234.5', opts)).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('allows whitespace', () => {
|
|
215
|
+
expect(isAllowedIntermediateInput(' 12 ')).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -26,12 +26,69 @@ export function parseNumberInput(display: string): number {
|
|
|
26
26
|
return NaN;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Drop whitespace and apostrophe grouping that may appear in pasted or
|
|
30
|
+
// locale-formatted numbers (e.g. `1 234`, `1'234`).
|
|
31
|
+
let normalized = trimmed.replace(/[\s']/g, '');
|
|
32
|
+
|
|
33
|
+
const commaCount = (normalized.match(/,/g) ?? []).length;
|
|
34
|
+
const dotCount = (normalized.match(/\./g) ?? []).length;
|
|
35
|
+
|
|
36
|
+
if (commaCount > 0 && dotCount > 0) {
|
|
37
|
+
// Both separators present: the right-most one is the decimal separator,
|
|
38
|
+
// the other is a grouping separator.
|
|
39
|
+
const decimalSeparator =
|
|
40
|
+
normalized.lastIndexOf(',') > normalized.lastIndexOf('.') ? ',' : '.';
|
|
41
|
+
const groupSeparator = decimalSeparator === ',' ? '.' : ',';
|
|
42
|
+
normalized = normalized.split(groupSeparator).join('');
|
|
43
|
+
normalized = normalized.replace(decimalSeparator, '.');
|
|
44
|
+
} else if (commaCount > 1) {
|
|
45
|
+
// Multiple commas without a dot can only be grouping separators.
|
|
46
|
+
normalized = normalized.split(',').join('');
|
|
47
|
+
} else if (commaCount === 1) {
|
|
48
|
+
// A single comma is treated as the decimal separator.
|
|
49
|
+
normalized = normalized.replace(',', '.');
|
|
50
|
+
} else if (dotCount > 1) {
|
|
51
|
+
// Multiple dots without a comma can only be grouping separators.
|
|
52
|
+
normalized = normalized.split('.').join('');
|
|
53
|
+
}
|
|
30
54
|
|
|
31
55
|
const n = Number(normalized);
|
|
32
56
|
return Number.isFinite(n) ? n : NaN;
|
|
33
57
|
}
|
|
34
58
|
|
|
59
|
+
function escapeRegex(value: string): string {
|
|
60
|
+
return value.replace(/[-.*+?^${}()|[\]\\]/g, '\\$&');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Permissive per-keystroke validator used to reject non-numeric characters
|
|
65
|
+
* before they reach the input. Allows half-typed states like `10.`, `-`, `,5`,
|
|
66
|
+
* `1,234.`; final correctness is enforced by `parseNumberInput` at commit.
|
|
67
|
+
*
|
|
68
|
+
* Accepts: optional leading sign, digits, the active decimal separator,
|
|
69
|
+
* the active group separator, and whitespace.
|
|
70
|
+
*/
|
|
71
|
+
export function isAllowedIntermediateInput(
|
|
72
|
+
value: string,
|
|
73
|
+
options: NumberInputFormatOptions = {}
|
|
74
|
+
): boolean {
|
|
75
|
+
if (value === '') return true;
|
|
76
|
+
|
|
77
|
+
const localeSeparators = getLocaleNumberSeparators();
|
|
78
|
+
const decimalSeparator =
|
|
79
|
+
options.decimalSeparator ?? localeSeparators.decimalSeparator;
|
|
80
|
+
const groupSeparator =
|
|
81
|
+
options.groupSeparator ?? localeSeparators.groupSeparator;
|
|
82
|
+
|
|
83
|
+
const allowedChars = new Set<string>([decimalSeparator]);
|
|
84
|
+
if (groupSeparator) {
|
|
85
|
+
allowedChars.add(groupSeparator);
|
|
86
|
+
}
|
|
87
|
+
const allowedClass = [...allowedChars].map(escapeRegex).join('') + '0-9 \\t';
|
|
88
|
+
const pattern = new RegExp(`^[+\\-]?[${allowedClass}]*$`);
|
|
89
|
+
return pattern.test(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
35
92
|
function clampFractionDigits(
|
|
36
93
|
minFractionDigits: number | undefined,
|
|
37
94
|
maxFractionDigits: number | undefined
|
|
@@ -110,7 +167,7 @@ export function removeGroupingFromDisplay(
|
|
|
110
167
|
return display;
|
|
111
168
|
}
|
|
112
169
|
|
|
113
|
-
const escaped = groupSeparator
|
|
170
|
+
const escaped = escapeRegex(groupSeparator);
|
|
114
171
|
return display.replace(new RegExp(escaped, 'g'), '');
|
|
115
172
|
}
|
|
116
173
|
|
|
@@ -41,9 +41,3 @@
|
|
|
41
41
|
:host([disabled]) .helper-text {
|
|
42
42
|
color: var(--element-disabled-color);
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
:host([disabled]) .field-wrapper.has-unit-slot .unit-slot {
|
|
46
|
-
color: var(--on-normal-disabled-color);
|
|
47
|
-
border-color: var(--normal-disabled-border-color);
|
|
48
|
-
background: var(--normal-disabled-background-color);
|
|
49
|
-
}
|