@bluevolt-tech/lumen 2.1.0 → 2.3.0
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/dist/components/_shared/date-range-validator.d.ts +26 -1
- package/dist/components/_shared/dropdown-controller.d.ts +45 -0
- package/dist/components/bv-date-range-input/bv-date-range-input.d.ts +7 -1
- package/dist/components/bv-date-range-input/bv-date-range-input.js +241 -96
- package/dist/components/bv-date-range-picker/bv-date-range-picker.d.ts +61 -8
- package/dist/components/bv-date-range-picker/bv-date-range-picker.js +180 -81
- package/dist/components/bv-filter-dropdown/bv-filter-dropdown.js +18 -10
- package/dist/components/bv-input-dropdown/bv-input-dropdown.js +18 -10
- package/dist/components/bv-search-picker/bv-search-picker.d.ts +222 -0
- package/dist/components/bv-search-picker/bv-search-picker.js +1458 -0
- package/dist/components/bv-segmented-control/bv-segmented-control.d.ts +7 -1
- package/dist/components/bv-segmented-control/bv-segmented-control.js +21 -2
- package/dist/components/bv-select/bv-select.js +18 -10
- package/dist/components/bv-split-button/bv-split-button.js +18 -10
- package/dist/components/bv-table/bv-table.js +1 -0
- package/dist/components/bv-tooltip/bv-tooltip.js +1 -0
- package/dist/storybook/stories/screens/cet-report.data.js +834 -31
- package/dist/themes/base.css +1 -1
- package/package.json +5 -1
- package/themes/base.css +1 -1
|
@@ -21,8 +21,7 @@ function adoptStyles(shadow, css) {
|
|
|
21
21
|
|
|
22
22
|
// components/_shared/dropdown-controller.ts
|
|
23
23
|
function bindDropdownController(host, options = {}) {
|
|
24
|
-
|
|
25
|
-
if (!shadow) {
|
|
24
|
+
if (!host.shadowRoot) {
|
|
26
25
|
throw new Error("bindDropdownController requires the host to have an open shadow root.");
|
|
27
26
|
}
|
|
28
27
|
const syncExpanded = () => {
|
|
@@ -37,24 +36,34 @@ function bindDropdownController(host, options = {}) {
|
|
|
37
36
|
syncExpanded();
|
|
38
37
|
host.dispatchEvent(new CustomEvent("bv-open", { bubbles: true, composed: true }));
|
|
39
38
|
};
|
|
40
|
-
const
|
|
39
|
+
const closeWithReason = (reason) => {
|
|
40
|
+
var _a;
|
|
41
41
|
if (!host.hasAttribute("open")) return;
|
|
42
|
+
if (options.canClose && !options.canClose(reason)) return;
|
|
42
43
|
host.removeAttribute("open");
|
|
43
44
|
syncExpanded();
|
|
45
|
+
host.dispatchEvent(
|
|
46
|
+
new CustomEvent("bv-dropdown-close", {
|
|
47
|
+
bubbles: true,
|
|
48
|
+
composed: true,
|
|
49
|
+
detail: { reason }
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
(_a = options.onAfterClose) == null ? void 0 : _a.call(options);
|
|
44
53
|
};
|
|
54
|
+
const close = () => closeWithReason("programmatic");
|
|
45
55
|
const toggle = () => {
|
|
46
|
-
if (host.hasAttribute("open"))
|
|
56
|
+
if (host.hasAttribute("open")) closeWithReason("programmatic");
|
|
47
57
|
else open();
|
|
48
58
|
};
|
|
49
59
|
const offClick = (e) => {
|
|
50
60
|
if (!host.hasAttribute("open")) return;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
close();
|
|
61
|
+
if (e.composedPath().includes(host)) return;
|
|
62
|
+
closeWithReason("outside-click");
|
|
54
63
|
};
|
|
55
64
|
document.addEventListener("click", offClick);
|
|
56
65
|
const onKeyDown = (e) => {
|
|
57
|
-
if (e.key === "Escape")
|
|
66
|
+
if (e.key === "Escape") closeWithReason("escape");
|
|
58
67
|
};
|
|
59
68
|
host.addEventListener("keydown", onKeyDown);
|
|
60
69
|
const observer = new MutationObserver(syncExpanded);
|
|
@@ -69,9 +78,16 @@ function bindDropdownController(host, options = {}) {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
// css-inline:/home/runner/work/BV_lumen/BV_lumen/components/bv-date-range-picker/bv-date-range-picker.css
|
|
72
|
-
var bv_date_range_picker_default = ":host {\n display: inline-block;\n font-family: var(--bv-font-family-base);\n}\n\n/* \u2500\u2500 Picker shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.picker {\n display: flex;\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-200);\n border-radius: var(--bv-radius-xl);\n box-shadow: var(--bv-shadow-lg);\n overflow: hidden;\n user-select: none;\n}\n\n/* \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sidebar {\n display: flex;\n flex-direction: column;\n width: 152px;\n flex-shrink: 0;\n padding: var(--bv-spacing-sm) 0;\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn {\n display: block;\n width: 100%;\n padding: 10px var(--bv-spacing-md);\n text-align: left;\n background: none;\n border: none;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n white-space: nowrap;\n}\n\n.preset-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.sidebar-sep {\n margin: var(--bv-spacing-xs) 0;\n border: none;\n border-top: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn.active {\n background: var(--bv-color-secondary-light);\n color: var(--bv-color-neutral-900);\n font-weight: var(--bv-font-weight-medium);\n}\n\n/* \u2500\u2500 Main panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.main {\n display: flex;\n flex-direction: column;\n flex: 1;\n}\n\n/* \u2500\u2500 Calendars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendars {\n display: flex;\n border-bottom: 1px solid var(--bv-color-neutral-200);\n}\n\n.calendar {\n padding: var(--bv-spacing-md) var(--bv-spacing-lg);\n flex: 1;\n}\n\n.calendar--left {\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n/* \u2500\u2500 Calendar header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendar-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: var(--bv-spacing-md);\n}\n\n.calendar-title {\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n}\n\n.nav-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n background: none;\n border: none;\n border-radius: var(--bv-radius-md);\n color: var(--bv-color-neutral-500);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n flex-shrink: 0;\n}\n\n.nav-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-800);\n}\n\n/* \u2500\u2500 Weekdays row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n margin-bottom: var(--bv-spacing-xs);\n}\n\n.weekday {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 28px;\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-medium);\n color: var(--bv-color-neutral-500);\n}\n\n/* \u2500\u2500 Days grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.days-grid {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n}\n\n/* \u2500\u2500 Day cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n border-radius: 0;\n transition: background var(--bv-transition-fast);\n}\n\n.day:hover:not(.start):not(.end) .day-inner {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.day--blank {\n pointer-events: none;\n}\n\n.day.today:not(.start):not(.end) .day-inner::after {\n content: '';\n position: absolute;\n bottom: 2px;\n left: 50%;\n transform: translateX(-50%);\n width: 4px;\n height: 4px;\n border-radius: var(--bv-radius-full);\n background: var(--bv-color-primary);\n}\n\n/* \u2500\u2500 Range band \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day.in-range {\n background: var(--bv-color-primary-light);\n}\n\n.day.in-range.week-start,\n.day.range-end.week-start {\n border-radius: var(--bv-radius-full) 0 0 var(--bv-radius-full);\n}\n\n.day.in-range.week-end,\n.day.range-start.week-end {\n border-radius: 0 var(--bv-radius-full) var(--bv-radius-full) 0;\n}\n\n.day.range-start {\n background: linear-gradient(to right, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n.day.range-end {\n background: linear-gradient(to left, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n/* \u2500\u2500 Day inner (circle) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day-inner {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: var(--bv-radius-full);\n z-index: 1;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.day.start .day-inner,\n.day.end .day-inner {\n background: var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n font-weight: var(--bv-font-weight-semibold);\n}\n\n.day.start:hover .day-inner,\n.day.end:hover .day-inner {\n background: var(--bv-color-primary-dark);\n}\n\n/* \u2500\u2500 5-year warning banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner-wrap {\n display: none;\n padding: var(--bv-spacing-lg);\n}\n\n.picker--with-banner .banner-wrap {\n display: block;\n}\n\n/* Footer loses its top padding when the banner is shown above it */\n.picker--with-banner .footer {\n padding-top: 0;\n}\n\n/* \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--bv-spacing-lg);\n gap: var(--bv-spacing-md);\n}\n\n/* \u2500\u2500 Date inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.date-inputs {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n}\n\n.date-input {\n width: 112px;\n height: 36px;\n padding: 0 var(--bv-spacing-sm);\n border: 1px solid var(--bv-color-neutral-300);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-700);\n text-align: center;\n outline: none;\n transition:\n border-color var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n.date-input:focus {\n border-color: var(--bv-color-primary);\n box-shadow: 0 0 0 3px var(--bv-color-primary-light);\n}\n\n.date-input::placeholder {\n color: var(--bv-color-neutral-400);\n}\n\n.date-sep {\n color: var(--bv-color-neutral-400);\n font-size: var(--bv-font-size-sm);\n}\n\n/* \u2500\u2500 Action buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer-actions {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n flex-shrink: 0;\n}\n\n.btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 36px;\n padding: 0 var(--bv-spacing-md);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n cursor: pointer;\n white-space: nowrap;\n transition:\n background var(--bv-transition-fast),\n border-color var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.btn-cancel {\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-300);\n color: var(--bv-color-neutral-700);\n}\n\n.btn-cancel:hover {\n background: var(--bv-color-secondary);\n border-color: var(--bv-color-neutral-400);\n}\n\n.btn-apply {\n background: var(--bv-color-primary);\n border: 1px solid var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n}\n\n.btn-apply:hover:not(:disabled) {\n background: var(--bv-color-primary-dark);\n border-color: var(--bv-color-primary-dark);\n}\n\n.btn-apply:disabled {\n background: var(--bv-color-neutral-300);\n border-color: var(--bv-color-neutral-300);\n cursor: not-allowed;\n}\n\n/* \u2500\u2500 BTN-10191 \xB7 validation error states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.date-input.invalid,\n.date-input.invalid:focus {\n border-color: var(--bv-color-danger);\n box-shadow: 0 0 0 3px var(--bv-color-danger-light);\n}\n\n.error-messages {\n padding: var(--bv-spacing-2xs) 0 0 0;\n}\n\n.input-error,\n.form-error {\n color: var(--bv-color-danger);\n font-size: var(--bv-font-size-xs);\n line-height: 1.25;\n margin-top: var(--bv-spacing-2xs);\n}\n\n.form-error {\n font-size: var(--bv-font-size-sm);\n margin-top: var(--bv-spacing-xs);\n}\n\n.day--disabled,\n.day[disabled] {\n color: var(--bv-color-neutral-300);\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.day--disabled .day-inner,\n.day[disabled] .day-inner {\n color: var(--bv-color-neutral-300);\n}\n";
|
|
81
|
+
var bv_date_range_picker_default = ":host {\n display: inline-block;\n font-family: var(--bv-font-family-base);\n}\n\n/* \u2500\u2500 Picker shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.picker {\n display: flex;\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-200);\n border-radius: var(--bv-radius-xl);\n box-shadow: var(--bv-shadow-lg);\n overflow: hidden;\n user-select: none;\n}\n\n/* \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sidebar {\n display: flex;\n flex-direction: column;\n width: 152px;\n flex-shrink: 0;\n padding: var(--bv-spacing-sm) 0;\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn {\n display: block;\n width: 100%;\n padding: 10px var(--bv-spacing-md);\n text-align: left;\n background: none;\n border: none;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n white-space: nowrap;\n}\n\n.preset-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.sidebar-sep {\n margin: var(--bv-spacing-xs) 0;\n border: none;\n border-top: 1px solid var(--bv-color-neutral-200);\n}\n\n.preset-btn.active {\n background: var(--bv-color-secondary-light);\n color: var(--bv-color-neutral-900);\n font-weight: var(--bv-font-weight-medium);\n}\n\n/* \u2500\u2500 Main panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.main {\n display: flex;\n flex-direction: column;\n flex: 1;\n}\n\n/* \u2500\u2500 Calendars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendars {\n display: flex;\n border-bottom: 1px solid var(--bv-color-neutral-200);\n}\n\n.calendar {\n padding: var(--bv-spacing-md) var(--bv-spacing-lg);\n flex: 1;\n}\n\n.calendar--left {\n border-right: 1px solid var(--bv-color-neutral-200);\n}\n\n/* \u2500\u2500 Calendar header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.calendar-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: var(--bv-spacing-md);\n}\n\n.calendar-title {\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n color: var(--bv-color-neutral-800);\n}\n\n.nav-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n background: none;\n border: none;\n border-radius: var(--bv-radius-md);\n color: var(--bv-color-neutral-500);\n cursor: pointer;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n flex-shrink: 0;\n}\n\n.nav-btn:hover {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-800);\n}\n\n/* \u2500\u2500 Weekdays row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.weekdays {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n margin-bottom: var(--bv-spacing-xs);\n}\n\n.weekday {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 28px;\n font-size: var(--bv-font-size-xs);\n font-weight: var(--bv-font-weight-medium);\n color: var(--bv-color-neutral-500);\n}\n\n/* \u2500\u2500 Days grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.days-grid {\n display: grid;\n grid-template-columns: repeat(7, 36px);\n gap: 0;\n}\n\n/* \u2500\u2500 Day cell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-regular);\n color: var(--bv-color-neutral-700);\n border-radius: 0;\n transition: background var(--bv-transition-fast);\n}\n\n.day:hover:not(.start):not(.end) .day-inner {\n background: var(--bv-color-secondary);\n color: var(--bv-color-neutral-900);\n}\n\n.day--blank {\n pointer-events: none;\n}\n\n.day.today:not(.start):not(.end) .day-inner::after {\n content: '';\n position: absolute;\n bottom: 2px;\n left: 50%;\n transform: translateX(-50%);\n width: 4px;\n height: 4px;\n border-radius: var(--bv-radius-full);\n background: var(--bv-color-primary);\n}\n\n/* \u2500\u2500 Range band \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day.in-range {\n background: var(--bv-color-primary-light);\n}\n\n.day.in-range.week-start,\n.day.range-end.week-start {\n border-radius: var(--bv-radius-full) 0 0 var(--bv-radius-full);\n}\n\n.day.in-range.week-end,\n.day.range-start.week-end {\n border-radius: 0 var(--bv-radius-full) var(--bv-radius-full) 0;\n}\n\n.day.range-start {\n background: linear-gradient(to right, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n.day.range-end {\n background: linear-gradient(to left, transparent 50%, var(--bv-color-primary-light) 50%);\n}\n\n/* \u2500\u2500 Day inner (circle) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.day-inner {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border-radius: var(--bv-radius-full);\n z-index: 1;\n transition:\n background var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.day.start .day-inner,\n.day.end .day-inner {\n background: var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n font-weight: var(--bv-font-weight-semibold);\n}\n\n.day.start:hover .day-inner,\n.day.end:hover .day-inner {\n background: var(--bv-color-primary-dark);\n}\n\n/* \u2500\u2500 Alert banner (BTN-10312 \u2014 unified for all validation errors) \u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.banner-wrap {\n display: none;\n padding: var(--bv-spacing-lg);\n}\n\n.picker--with-banner .banner-wrap {\n display: block;\n}\n\n/* Footer loses its top padding when the banner is shown above it */\n.picker--with-banner .footer {\n padding-top: 0;\n}\n\n/* \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--bv-spacing-lg);\n gap: var(--bv-spacing-md);\n}\n\n/* \u2500\u2500 Date inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.date-inputs {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n}\n\n.date-input {\n width: 112px;\n height: 36px;\n padding: 0 var(--bv-spacing-sm);\n border: 1px solid var(--bv-color-neutral-300);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n color: var(--bv-color-neutral-700);\n text-align: center;\n outline: none;\n transition:\n border-color var(--bv-transition-fast),\n box-shadow var(--bv-transition-fast);\n}\n\n.date-input:focus {\n border-color: var(--bv-color-primary);\n box-shadow: 0 0 0 3px var(--bv-color-primary-light);\n}\n\n.date-input::placeholder {\n color: var(--bv-color-neutral-400);\n}\n\n.date-sep {\n color: var(--bv-color-neutral-400);\n font-size: var(--bv-font-size-sm);\n}\n\n/* \u2500\u2500 Action buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.footer-actions {\n display: flex;\n align-items: center;\n gap: var(--bv-spacing-sm);\n flex-shrink: 0;\n}\n\n.btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 36px;\n padding: 0 var(--bv-spacing-md);\n border-radius: var(--bv-radius-lg);\n font-family: inherit;\n font-size: var(--bv-font-size-sm);\n font-weight: var(--bv-font-weight-semibold);\n cursor: pointer;\n white-space: nowrap;\n transition:\n background var(--bv-transition-fast),\n border-color var(--bv-transition-fast),\n color var(--bv-transition-fast);\n}\n\n.btn-cancel {\n background: var(--bv-color-neutral-0);\n border: 1px solid var(--bv-color-neutral-300);\n color: var(--bv-color-neutral-700);\n}\n\n.btn-cancel:hover {\n background: var(--bv-color-secondary);\n border-color: var(--bv-color-neutral-400);\n}\n\n.btn-apply {\n background: var(--bv-color-primary);\n border: 1px solid var(--bv-color-primary);\n color: var(--bv-color-neutral-0);\n}\n\n.btn-apply:hover:not(:disabled) {\n background: var(--bv-color-primary-dark);\n border-color: var(--bv-color-primary-dark);\n}\n\n.btn-apply:disabled {\n background: var(--bv-color-neutral-300);\n border-color: var(--bv-color-neutral-300);\n cursor: not-allowed;\n}\n\n/* \u2500\u2500 Validation error states (BTN-10191 / BTN-10312) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/*\n * `.invalid` is applied to input fields only when the field's error resolves\n * to severity 'error' (blocking). Non-blocking severities (warning/primary/\n * success) surface via the banner but do not paint the input red.\n */\n.date-input.invalid,\n.date-input.invalid:focus {\n border-color: var(--bv-color-danger);\n box-shadow: 0 0 0 3px var(--bv-color-danger-light);\n}\n\n.day--disabled,\n.day[disabled] {\n color: var(--bv-color-neutral-300);\n cursor: not-allowed;\n pointer-events: none;\n}\n\n.day--disabled .day-inner,\n.day[disabled] .day-inner {\n color: var(--bv-color-neutral-300);\n}\n";
|
|
73
82
|
|
|
74
83
|
// components/_shared/date-range-validator.ts
|
|
84
|
+
var ALLOWED_SEVERITIES = ["error", "warning", "primary", "success"];
|
|
85
|
+
function normalizeSeverity(value) {
|
|
86
|
+
if (value && ALLOWED_SEVERITIES.includes(value)) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
return "error";
|
|
90
|
+
}
|
|
75
91
|
function validateDateRange(input, config = {}) {
|
|
76
92
|
const { start, end } = input;
|
|
77
93
|
const { min, max, maxRangeYears, validateOrder, validateFormat } = config;
|
|
@@ -102,7 +118,11 @@ function validateDateRange(input, config = {}) {
|
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
const valid = !startError && !endError && !formError;
|
|
105
|
-
const
|
|
121
|
+
const startSev = startError ? severityOfFieldError(startError, config) : null;
|
|
122
|
+
const endSev = endError ? severityOfFieldError(endError, config) : null;
|
|
123
|
+
const formSev = formError ? severityOfFormError(formError, config) : null;
|
|
124
|
+
const hasBlockingError = startSev === "error" || endSev === "error" || formSev === "error";
|
|
125
|
+
const applyEnabled = !hasBlockingError && !!startDate && !!endDate;
|
|
106
126
|
return {
|
|
107
127
|
valid,
|
|
108
128
|
applyEnabled,
|
|
@@ -111,6 +131,15 @@ function validateDateRange(input, config = {}) {
|
|
|
111
131
|
...formError && { formError }
|
|
112
132
|
};
|
|
113
133
|
}
|
|
134
|
+
function severityOfFieldError(err, config) {
|
|
135
|
+
if (err === "format") return normalizeSeverity(config.formatSeverity);
|
|
136
|
+
if (err === "min") return normalizeSeverity(config.minSeverity);
|
|
137
|
+
return normalizeSeverity(config.maxSeverity);
|
|
138
|
+
}
|
|
139
|
+
function severityOfFormError(err, config) {
|
|
140
|
+
if (err === "order") return normalizeSeverity(config.orderSeverity);
|
|
141
|
+
return normalizeSeverity(config.rangeLimitSeverity);
|
|
142
|
+
}
|
|
114
143
|
function parseIso(s) {
|
|
115
144
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
116
145
|
if (!m) return null;
|
|
@@ -437,7 +466,13 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
437
466
|
"error-order",
|
|
438
467
|
"error-format",
|
|
439
468
|
"range-limit-message",
|
|
440
|
-
"range-limit-heading"
|
|
469
|
+
"range-limit-heading",
|
|
470
|
+
// BTN-10312 — per-error severity (default error = blocks Apply + red banner)
|
|
471
|
+
"error-min-severity",
|
|
472
|
+
"error-max-severity",
|
|
473
|
+
"error-format-severity",
|
|
474
|
+
"error-order-severity",
|
|
475
|
+
"range-limit-severity"
|
|
441
476
|
];
|
|
442
477
|
_rendered = false;
|
|
443
478
|
_today;
|
|
@@ -461,12 +496,22 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
461
496
|
_startInput = null;
|
|
462
497
|
_endInput = null;
|
|
463
498
|
_applyBtn = null;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Unified alert banner (BTN-10312). Shows AT MOST ONE error at a time. When
|
|
501
|
+
* multiple errors are simultaneously active, the banner reflects the
|
|
502
|
+
* most-recently-edited field (see `_lastEditedField`).
|
|
503
|
+
*/
|
|
504
|
+
_alertBannerEl = null;
|
|
468
505
|
_leftDayMap = /* @__PURE__ */ new Map();
|
|
469
506
|
_rightDayMap = /* @__PURE__ */ new Map();
|
|
507
|
+
/**
|
|
508
|
+
* BTN-10312 — tracks which field the user most-recently interacted with.
|
|
509
|
+
* When both `startError` and `endError` are active, the banner shows the
|
|
510
|
+
* error corresponding to this field. Set to `null` when no field has been
|
|
511
|
+
* touched (initial render, or external attribute set); the resolver falls
|
|
512
|
+
* back to a stable precedence in that case.
|
|
513
|
+
*/
|
|
514
|
+
_lastEditedField = null;
|
|
470
515
|
constructor() {
|
|
471
516
|
super();
|
|
472
517
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -501,10 +546,12 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
501
546
|
if (name === "start") {
|
|
502
547
|
this._start = val ?? "";
|
|
503
548
|
this._startRawInvalid = "";
|
|
549
|
+
this._lastEditedField = null;
|
|
504
550
|
}
|
|
505
551
|
if (name === "end") {
|
|
506
552
|
this._end = val ?? "";
|
|
507
553
|
this._endRawInvalid = "";
|
|
554
|
+
this._lastEditedField = null;
|
|
508
555
|
}
|
|
509
556
|
if (name === "preset") this._preset = val ?? "";
|
|
510
557
|
if (!this._rendered) return;
|
|
@@ -515,6 +562,30 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
515
562
|
}
|
|
516
563
|
this._syncValidity();
|
|
517
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* BTN-10312 — public reset of the picker's internal draft state to the
|
|
567
|
+
* currently committed `start` / `end` / `preset` attributes. Called by
|
|
568
|
+
* `bv-date-range-input` when the popup closes without Apply (Cancel,
|
|
569
|
+
* Escape, outside-click) so the next open shows the last-committed value
|
|
570
|
+
* cleanly, with no leftover error banners from the discarded edit.
|
|
571
|
+
*
|
|
572
|
+
* The external value is not modified — this only clears the draft.
|
|
573
|
+
*/
|
|
574
|
+
resetDraft() {
|
|
575
|
+
this._start = this.getAttribute("start") ?? "";
|
|
576
|
+
this._end = this.getAttribute("end") ?? "";
|
|
577
|
+
this._preset = this.getAttribute("preset") ?? "";
|
|
578
|
+
this._startRawInvalid = "";
|
|
579
|
+
this._endRawInvalid = "";
|
|
580
|
+
this._picking = false;
|
|
581
|
+
this._hover = "";
|
|
582
|
+
this._lastEditedField = null;
|
|
583
|
+
if (!this._rendered) return;
|
|
584
|
+
this._syncCalendars();
|
|
585
|
+
this._syncInputs();
|
|
586
|
+
this._syncPresets();
|
|
587
|
+
this._syncValidity();
|
|
588
|
+
}
|
|
518
589
|
_render() {
|
|
519
590
|
if (this._rendered) return;
|
|
520
591
|
this._rendered = true;
|
|
@@ -598,31 +669,11 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
598
669
|
this._applyBtn = applyBtn;
|
|
599
670
|
actions.append(cancelBtn, applyBtn);
|
|
600
671
|
footer.append(inputsWrap, actions);
|
|
601
|
-
const errorsWrap = document.createElement("div");
|
|
602
|
-
errorsWrap.className = "error-messages";
|
|
603
|
-
const startErrorEl = document.createElement("div");
|
|
604
|
-
startErrorEl.className = "input-error input-error--start";
|
|
605
|
-
startErrorEl.setAttribute("role", "alert");
|
|
606
|
-
startErrorEl.hidden = true;
|
|
607
|
-
this._startErrorEl = startErrorEl;
|
|
608
|
-
const endErrorEl = document.createElement("div");
|
|
609
|
-
endErrorEl.className = "input-error input-error--end";
|
|
610
|
-
endErrorEl.setAttribute("role", "alert");
|
|
611
|
-
endErrorEl.hidden = true;
|
|
612
|
-
this._endErrorEl = endErrorEl;
|
|
613
|
-
const formErrorEl = document.createElement("div");
|
|
614
|
-
formErrorEl.className = "form-error";
|
|
615
|
-
formErrorEl.setAttribute("role", "alert");
|
|
616
|
-
formErrorEl.hidden = true;
|
|
617
|
-
this._formErrorEl = formErrorEl;
|
|
618
|
-
errorsWrap.append(startErrorEl, endErrorEl, formErrorEl);
|
|
619
|
-
footer.insertBefore(errorsWrap, actions);
|
|
620
672
|
const bannerWrap = document.createElement("div");
|
|
621
673
|
bannerWrap.className = "banner-wrap";
|
|
622
674
|
const banner = document.createElement("bv-alert-banner");
|
|
623
|
-
banner.setAttribute("variant", "warning");
|
|
624
675
|
banner.hidden = true;
|
|
625
|
-
this.
|
|
676
|
+
this._alertBannerEl = banner;
|
|
626
677
|
bannerWrap.appendChild(banner);
|
|
627
678
|
main.append(calsWrap, bannerWrap, footer);
|
|
628
679
|
picker.append(sidebar, main);
|
|
@@ -795,6 +846,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
795
846
|
this._preset = "custom";
|
|
796
847
|
this._picking = true;
|
|
797
848
|
this._hover = "";
|
|
849
|
+
this._lastEditedField = "start";
|
|
798
850
|
} else {
|
|
799
851
|
if (iso < this._start) {
|
|
800
852
|
this._end = this._start;
|
|
@@ -805,6 +857,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
805
857
|
this._picking = false;
|
|
806
858
|
this._hover = "";
|
|
807
859
|
this._preset = "custom";
|
|
860
|
+
this._lastEditedField = "end";
|
|
808
861
|
}
|
|
809
862
|
this._startRawInvalid = "";
|
|
810
863
|
this._endRawInvalid = "";
|
|
@@ -829,6 +882,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
829
882
|
this._hover = "";
|
|
830
883
|
this._startRawInvalid = "";
|
|
831
884
|
this._endRawInvalid = "";
|
|
885
|
+
this._lastEditedField = null;
|
|
832
886
|
if (start) {
|
|
833
887
|
const parts = start.split("-");
|
|
834
888
|
const y = parseInt(parts[0], 10);
|
|
@@ -844,6 +898,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
844
898
|
this._syncValidity();
|
|
845
899
|
}
|
|
846
900
|
_onInputBlur(input, field) {
|
|
901
|
+
this._lastEditedField = field;
|
|
847
902
|
const raw = input.value;
|
|
848
903
|
if (raw === "") {
|
|
849
904
|
if (field === "start") {
|
|
@@ -907,9 +962,9 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
907
962
|
},
|
|
908
963
|
cfg
|
|
909
964
|
);
|
|
910
|
-
this.
|
|
965
|
+
this._paintInputInvalidState();
|
|
966
|
+
this._applyAlertBanner(cfg);
|
|
911
967
|
this._applyApplyState();
|
|
912
|
-
this._applyBannerState();
|
|
913
968
|
this._applyCalendarDisabledState();
|
|
914
969
|
if (previous.valid !== this._validity.valid || previous.applyEnabled !== this._validity.applyEnabled || previous.startError !== this._validity.startError || previous.endError !== this._validity.endError || previous.formError !== this._validity.formError) {
|
|
915
970
|
this.dispatchEvent(
|
|
@@ -931,88 +986,140 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
931
986
|
max,
|
|
932
987
|
maxRangeYears: Number.isFinite(maxRangeYears) ? maxRangeYears : void 0,
|
|
933
988
|
validateOrder: this.hasAttribute("validate-order"),
|
|
934
|
-
validateFormat: this.hasAttribute("validate-format")
|
|
989
|
+
validateFormat: this.hasAttribute("validate-format"),
|
|
990
|
+
minSeverity: normalizeSeverity(this.getAttribute("error-min-severity")),
|
|
991
|
+
maxSeverity: normalizeSeverity(this.getAttribute("error-max-severity")),
|
|
992
|
+
formatSeverity: normalizeSeverity(this.getAttribute("error-format-severity")),
|
|
993
|
+
orderSeverity: normalizeSeverity(this.getAttribute("error-order-severity")),
|
|
994
|
+
rangeLimitSeverity: normalizeSeverity(this.getAttribute("range-limit-severity"))
|
|
935
995
|
};
|
|
936
996
|
}
|
|
937
|
-
|
|
997
|
+
/**
|
|
998
|
+
* BTN-10312 — apply the `.invalid` + `aria-invalid` state to input fields
|
|
999
|
+
* whose field-level error is currently blocking (severity `'error'`).
|
|
1000
|
+
* Non-blocking severities (warning/primary/success) do not paint the input
|
|
1001
|
+
* red so the visual matches the semantic ("not a fatal error").
|
|
1002
|
+
*/
|
|
1003
|
+
_paintInputInvalidState() {
|
|
938
1004
|
const v = this._validity;
|
|
939
|
-
this.
|
|
940
|
-
this.
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1005
|
+
const cfg = this._readConfig();
|
|
1006
|
+
this._paintOne(this._startInput, v.startError ? severityOfFieldError(v.startError, cfg) : null);
|
|
1007
|
+
this._paintOne(this._endInput, v.endError ? severityOfFieldError(v.endError, cfg) : null);
|
|
1008
|
+
}
|
|
1009
|
+
_paintOne(input, sev) {
|
|
1010
|
+
if (!input) return;
|
|
1011
|
+
if (sev === "error") {
|
|
1012
|
+
input.classList.add("invalid");
|
|
1013
|
+
input.setAttribute("aria-invalid", "true");
|
|
1014
|
+
} else {
|
|
1015
|
+
input.classList.remove("invalid");
|
|
1016
|
+
input.removeAttribute("aria-invalid");
|
|
949
1017
|
}
|
|
950
1018
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* BTN-10312 — single alert banner. Picks the "active" error following the
|
|
1021
|
+
* precedence:
|
|
1022
|
+
*
|
|
1023
|
+
* 1. If a form error is active, show it (form errors don't coexist with
|
|
1024
|
+
* field errors by validator design).
|
|
1025
|
+
* 2. If both field errors are active, show the one for
|
|
1026
|
+
* `_lastEditedField` (falling back to `start` if `null`).
|
|
1027
|
+
* 3. Else show whichever single field error is active.
|
|
1028
|
+
* 4. Else hide the banner.
|
|
1029
|
+
*
|
|
1030
|
+
* `show-range-limit-banner="false"` still suppresses the range-limit
|
|
1031
|
+
* banner specifically (existing consumer contract from BTN-10191).
|
|
1032
|
+
*/
|
|
1033
|
+
_applyAlertBanner(cfg) {
|
|
1034
|
+
if (!this._alertBannerEl) return;
|
|
1035
|
+
const pickerEl = this.shadowRoot.querySelector(".picker");
|
|
1036
|
+
const active = this._resolveActiveError();
|
|
1037
|
+
if (!active) {
|
|
1038
|
+
this._alertBannerEl.hidden = true;
|
|
1039
|
+
this._alertBannerEl.removeAttribute("heading");
|
|
1040
|
+
this._alertBannerEl.removeAttribute("message");
|
|
1041
|
+
pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
|
|
1042
|
+
return;
|
|
960
1043
|
}
|
|
961
|
-
if (
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
} else {
|
|
966
|
-
errorEl.textContent = "";
|
|
967
|
-
errorEl.hidden = true;
|
|
968
|
-
}
|
|
1044
|
+
if (active.kind === "form" && active.error === "range-limit" && this.getAttribute("show-range-limit-banner") === "false") {
|
|
1045
|
+
this._alertBannerEl.hidden = true;
|
|
1046
|
+
pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
|
|
1047
|
+
return;
|
|
969
1048
|
}
|
|
1049
|
+
const variant = active.kind === "field" ? severityOfFieldError(active.error, cfg) : severityOfFormError(active.error, cfg);
|
|
1050
|
+
this._alertBannerEl.setAttribute("variant", variant);
|
|
1051
|
+
if (active.kind === "form" && active.error === "range-limit") {
|
|
1052
|
+
const yearsAttr = this.getAttribute("max-range-years") ?? "";
|
|
1053
|
+
const heading = this.getAttribute("range-limit-heading") || (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
|
|
1054
|
+
const message = this.getAttribute("range-limit-message") || "Only CSV download is available for this date range. Live report generation is not supported.";
|
|
1055
|
+
this._alertBannerEl.setAttribute("heading", heading.replace(/\{N\}/g, yearsAttr));
|
|
1056
|
+
this._alertBannerEl.setAttribute("message", message.replace(/\{N\}/g, yearsAttr));
|
|
1057
|
+
} else {
|
|
1058
|
+
this._alertBannerEl.removeAttribute("heading");
|
|
1059
|
+
const message = active.kind === "field" ? this._resolveFieldError(active.error, active.field) : this._resolveOrderError();
|
|
1060
|
+
this._alertBannerEl.setAttribute("message", message);
|
|
1061
|
+
}
|
|
1062
|
+
this._alertBannerEl.hidden = false;
|
|
1063
|
+
pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Pick which error the banner should reflect. See `_applyAlertBanner` for
|
|
1067
|
+
* the precedence rules.
|
|
1068
|
+
*/
|
|
1069
|
+
_resolveActiveError() {
|
|
1070
|
+
const v = this._validity;
|
|
1071
|
+
if (v.formError) return { kind: "form", error: v.formError };
|
|
1072
|
+
if (v.startError && v.endError) {
|
|
1073
|
+
const winner = this._lastEditedField ?? "start";
|
|
1074
|
+
return {
|
|
1075
|
+
kind: "field",
|
|
1076
|
+
error: winner === "start" ? v.startError : v.endError,
|
|
1077
|
+
field: winner
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
if (v.startError) return { kind: "field", error: v.startError, field: "start" };
|
|
1081
|
+
if (v.endError) return { kind: "field", error: v.endError, field: "end" };
|
|
1082
|
+
return null;
|
|
970
1083
|
}
|
|
971
1084
|
_resolveFieldError(err, field) {
|
|
972
1085
|
if (err === "format") {
|
|
973
|
-
|
|
1086
|
+
const custom2 = this.getAttribute("error-format");
|
|
1087
|
+
if (custom2) return this._interpolateField(custom2, field);
|
|
1088
|
+
return `Enter a valid ${field} date`;
|
|
974
1089
|
}
|
|
975
1090
|
if (err === "min") {
|
|
976
1091
|
const minIso = this.getAttribute("min") ?? "";
|
|
977
1092
|
const minDisplay = formatDisplay(minIso);
|
|
978
1093
|
const custom2 = this.getAttribute("error-min");
|
|
979
|
-
if (custom2) return
|
|
1094
|
+
if (custom2) return this._interpolateField(custom2, field).replace(/\{min\}/g, minDisplay);
|
|
980
1095
|
return `${capitalize(field)} date must be on or after ${minDisplay}`;
|
|
981
1096
|
}
|
|
982
1097
|
const maxAttr = this.getAttribute("max") ?? "";
|
|
983
1098
|
const resolvedMax = maxAttr === "today" ? isoToday() : maxAttr;
|
|
984
1099
|
const maxDisplay = formatDisplay(resolvedMax);
|
|
985
1100
|
const custom = this.getAttribute("error-max");
|
|
986
|
-
if (custom) return
|
|
1101
|
+
if (custom) return this._interpolateField(custom, field).replace(/\{max\}/g, maxDisplay);
|
|
987
1102
|
if (maxAttr === "today") return `${capitalize(field)} date cannot be in the future`;
|
|
988
1103
|
return `${capitalize(field)} date cannot be after ${maxDisplay}`;
|
|
989
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* BTN-10312 — interpolate `{field}` (always lowercase) and `{Field}` (always
|
|
1107
|
+
* capitalized) in a user-provided template. Before this change, the historical
|
|
1108
|
+
* behavior was that `{field}` in custom copy resolved lowercase while the
|
|
1109
|
+
* built-in defaults capitalized inline — inconsistent. Now the split is
|
|
1110
|
+
* explicit: `{field}` = lowercase, `{Field}` = capitalized.
|
|
1111
|
+
*/
|
|
1112
|
+
_interpolateField(template, field) {
|
|
1113
|
+
return template.replace(/\{Field\}/g, capitalize(field)).replace(/\{field\}/g, field);
|
|
1114
|
+
}
|
|
990
1115
|
_resolveOrderError() {
|
|
991
|
-
return this.getAttribute("error-order")
|
|
1116
|
+
return this.getAttribute("error-order") || "Start date must be on or before end date";
|
|
992
1117
|
}
|
|
993
1118
|
_applyApplyState() {
|
|
994
1119
|
if (this._applyBtn) {
|
|
995
1120
|
this._applyBtn.disabled = !this._validity.applyEnabled;
|
|
996
1121
|
}
|
|
997
1122
|
}
|
|
998
|
-
_applyBannerState() {
|
|
999
|
-
if (!this._bannerEl) return;
|
|
1000
|
-
const pickerEl = this.shadowRoot.querySelector(".picker");
|
|
1001
|
-
const bannerAllowed = this.getAttribute("show-range-limit-banner") !== "false";
|
|
1002
|
-
const show = this._validity.formError === "range-limit" && bannerAllowed;
|
|
1003
|
-
if (show) {
|
|
1004
|
-
const yearsAttr = this.getAttribute("max-range-years") ?? "";
|
|
1005
|
-
const heading = this.getAttribute("range-limit-heading") ?? (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
|
|
1006
|
-
const message = this.getAttribute("range-limit-message") ?? "Only CSV download is available for this date range. Live report generation is not supported.";
|
|
1007
|
-
this._bannerEl.setAttribute("heading", heading.replace("{N}", yearsAttr));
|
|
1008
|
-
this._bannerEl.setAttribute("message", message.replace("{N}", yearsAttr));
|
|
1009
|
-
this._bannerEl.hidden = false;
|
|
1010
|
-
pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
|
|
1011
|
-
} else {
|
|
1012
|
-
this._bannerEl.hidden = true;
|
|
1013
|
-
pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
1123
|
_applyCalendarDisabledState() {
|
|
1017
1124
|
const minIso = this.getAttribute("min") || "";
|
|
1018
1125
|
const maxAttr = this.getAttribute("max") || "";
|
|
@@ -1048,6 +1155,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
1048
1155
|
var _a;
|
|
1049
1156
|
const ev = e;
|
|
1050
1157
|
if ((_a = ev.inputType) == null ? void 0 : _a.startsWith("delete")) return;
|
|
1158
|
+
this._lastEditedField = field;
|
|
1051
1159
|
const digits = input.value.replace(/[^\d]/g, "");
|
|
1052
1160
|
let formatted;
|
|
1053
1161
|
if (digits.length <= 2) {
|
|
@@ -1106,7 +1214,13 @@ var PICKER_PASSTHROUGH_ATTRS = [
|
|
|
1106
1214
|
"error-order",
|
|
1107
1215
|
"error-format",
|
|
1108
1216
|
"range-limit-message",
|
|
1109
|
-
"range-limit-heading"
|
|
1217
|
+
"range-limit-heading",
|
|
1218
|
+
// BTN-10312 — per-error severity attributes
|
|
1219
|
+
"error-min-severity",
|
|
1220
|
+
"error-max-severity",
|
|
1221
|
+
"error-format-severity",
|
|
1222
|
+
"error-order-severity",
|
|
1223
|
+
"range-limit-severity"
|
|
1110
1224
|
];
|
|
1111
1225
|
function isoToDisplay(iso) {
|
|
1112
1226
|
if (!iso) return "";
|
|
@@ -1120,6 +1234,8 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1120
1234
|
"end",
|
|
1121
1235
|
"placeholder",
|
|
1122
1236
|
"required",
|
|
1237
|
+
// BTN-10312 — local (not passed through to picker); gates popup close.
|
|
1238
|
+
"keep-open-while-invalid",
|
|
1123
1239
|
...PICKER_PASSTHROUGH_ATTRS
|
|
1124
1240
|
];
|
|
1125
1241
|
_rendered = false;
|
|
@@ -1128,6 +1244,12 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1128
1244
|
_triggerEl = null;
|
|
1129
1245
|
_pickerEl = null;
|
|
1130
1246
|
_dropdown = null;
|
|
1247
|
+
/**
|
|
1248
|
+
* BTN-10312 — cached validity from the internal picker. `canClose` on the
|
|
1249
|
+
* dropdown controller consults this to gate popup close when
|
|
1250
|
+
* `keep-open-while-invalid` is set.
|
|
1251
|
+
*/
|
|
1252
|
+
_pickerValidity = { valid: true, applyEnabled: false };
|
|
1131
1253
|
constructor() {
|
|
1132
1254
|
super();
|
|
1133
1255
|
adoptStyles(this.attachShadow({ mode: "open" }), bv_date_range_input_default);
|
|
@@ -1188,9 +1310,8 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1188
1310
|
icon.className = "trigger-icon";
|
|
1189
1311
|
icon.innerHTML = CALENDAR_ICON;
|
|
1190
1312
|
trigger.append(display, icon);
|
|
1191
|
-
trigger.addEventListener("click", (
|
|
1313
|
+
trigger.addEventListener("click", () => {
|
|
1192
1314
|
var _a;
|
|
1193
|
-
e.stopPropagation();
|
|
1194
1315
|
(_a = this._dropdown) == null ? void 0 : _a.toggle();
|
|
1195
1316
|
});
|
|
1196
1317
|
const popup = document.createElement("div");
|
|
@@ -1223,13 +1344,15 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1223
1344
|
});
|
|
1224
1345
|
picker.addEventListener("bv-validity-change", (e) => {
|
|
1225
1346
|
const detail = e.detail;
|
|
1347
|
+
this._pickerValidity = detail;
|
|
1348
|
+
const hasBlockingError = !detail.applyEnabled && !!(detail.startError || detail.endError || detail.formError);
|
|
1226
1349
|
if (this._triggerEl) {
|
|
1227
|
-
if (
|
|
1228
|
-
this._triggerEl.classList.remove("invalid");
|
|
1229
|
-
this._triggerEl.removeAttribute("aria-invalid");
|
|
1230
|
-
} else {
|
|
1350
|
+
if (hasBlockingError) {
|
|
1231
1351
|
this._triggerEl.classList.add("invalid");
|
|
1232
1352
|
this._triggerEl.setAttribute("aria-invalid", "true");
|
|
1353
|
+
} else {
|
|
1354
|
+
this._triggerEl.classList.remove("invalid");
|
|
1355
|
+
this._triggerEl.removeAttribute("aria-invalid");
|
|
1233
1356
|
}
|
|
1234
1357
|
}
|
|
1235
1358
|
this.dispatchEvent(
|
|
@@ -1251,6 +1374,28 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1251
1374
|
this._pickerEl.setAttribute("start", this.getAttribute("start") ?? "");
|
|
1252
1375
|
this._pickerEl.setAttribute("end", this.getAttribute("end") ?? "");
|
|
1253
1376
|
}
|
|
1377
|
+
},
|
|
1378
|
+
// BTN-10312 — gate popup close when the consumer opted in via
|
|
1379
|
+
// `keep-open-while-invalid` AND the picker has a blocking error.
|
|
1380
|
+
// Only *implicit* dismissals (Escape / outside-click) are gated —
|
|
1381
|
+
// explicit user actions (Cancel button, Apply, trigger toggle) always
|
|
1382
|
+
// close so the user can never get trapped in an invalid popup.
|
|
1383
|
+
// Non-blocking errors (warning/primary/success severity) do NOT block
|
|
1384
|
+
// close either: the value externally is still the last-committed one.
|
|
1385
|
+
canClose: (reason) => {
|
|
1386
|
+
if (reason === "programmatic") return true;
|
|
1387
|
+
if (!this.hasAttribute("keep-open-while-invalid")) return true;
|
|
1388
|
+
const v = this._pickerValidity;
|
|
1389
|
+
const hasBlockingError = !v.applyEnabled && !!(v.startError || v.endError || v.formError);
|
|
1390
|
+
return !hasBlockingError;
|
|
1391
|
+
},
|
|
1392
|
+
// BTN-10312 — after every successful close (including Apply, Cancel,
|
|
1393
|
+
// Escape, outside-click), reset the picker's internal draft state so the
|
|
1394
|
+
// next open shows the last-committed value cleanly. Apply cases end up
|
|
1395
|
+
// being a no-op because the draft already equals the committed value.
|
|
1396
|
+
onAfterClose: () => {
|
|
1397
|
+
var _a;
|
|
1398
|
+
(_a = this._pickerEl) == null ? void 0 : _a.resetDraft();
|
|
1254
1399
|
}
|
|
1255
1400
|
});
|
|
1256
1401
|
}
|