@bluevolt-tech/lumen 2.1.0 → 2.2.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 +29 -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 +233 -94
- 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 +10 -8
- package/dist/components/bv-input-dropdown/bv-input-dropdown.js +10 -8
- package/dist/components/bv-select/bv-select.js +10 -8
- package/dist/components/bv-split-button/bv-split-button.js +10 -8
- package/dist/components/bv-table/bv-table.js +1 -0
- package/dist/components/bv-tooltip/bv-tooltip.js +1 -0
- package/dist/themes/base.css +1 -1
- package/package.json +1 -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,27 @@ 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
|
+
(_a = options.onAfterClose) == null ? void 0 : _a.call(options);
|
|
44
46
|
};
|
|
47
|
+
const close = () => closeWithReason("programmatic");
|
|
45
48
|
const toggle = () => {
|
|
46
|
-
if (host.hasAttribute("open"))
|
|
49
|
+
if (host.hasAttribute("open")) closeWithReason("programmatic");
|
|
47
50
|
else open();
|
|
48
51
|
};
|
|
49
52
|
const offClick = (e) => {
|
|
50
53
|
if (!host.hasAttribute("open")) return;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
close();
|
|
54
|
+
if (e.composedPath().includes(host)) return;
|
|
55
|
+
closeWithReason("outside-click");
|
|
54
56
|
};
|
|
55
57
|
document.addEventListener("click", offClick);
|
|
56
58
|
const onKeyDown = (e) => {
|
|
57
|
-
if (e.key === "Escape")
|
|
59
|
+
if (e.key === "Escape") closeWithReason("escape");
|
|
58
60
|
};
|
|
59
61
|
host.addEventListener("keydown", onKeyDown);
|
|
60
62
|
const observer = new MutationObserver(syncExpanded);
|
|
@@ -69,9 +71,16 @@ function bindDropdownController(host, options = {}) {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
// 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";
|
|
74
|
+
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
75
|
|
|
74
76
|
// components/_shared/date-range-validator.ts
|
|
77
|
+
var ALLOWED_SEVERITIES = ["error", "warning", "primary", "success"];
|
|
78
|
+
function normalizeSeverity(value) {
|
|
79
|
+
if (value && ALLOWED_SEVERITIES.includes(value)) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
return "error";
|
|
83
|
+
}
|
|
75
84
|
function validateDateRange(input, config = {}) {
|
|
76
85
|
const { start, end } = input;
|
|
77
86
|
const { min, max, maxRangeYears, validateOrder, validateFormat } = config;
|
|
@@ -102,7 +111,11 @@ function validateDateRange(input, config = {}) {
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
const valid = !startError && !endError && !formError;
|
|
105
|
-
const
|
|
114
|
+
const startSev = startError ? severityOfFieldError(startError, config) : null;
|
|
115
|
+
const endSev = endError ? severityOfFieldError(endError, config) : null;
|
|
116
|
+
const formSev = formError ? severityOfFormError(formError, config) : null;
|
|
117
|
+
const hasBlockingError = startSev === "error" || endSev === "error" || formSev === "error";
|
|
118
|
+
const applyEnabled = !hasBlockingError && !!startDate && !!endDate;
|
|
106
119
|
return {
|
|
107
120
|
valid,
|
|
108
121
|
applyEnabled,
|
|
@@ -111,6 +124,15 @@ function validateDateRange(input, config = {}) {
|
|
|
111
124
|
...formError && { formError }
|
|
112
125
|
};
|
|
113
126
|
}
|
|
127
|
+
function severityOfFieldError(err, config) {
|
|
128
|
+
if (err === "format") return normalizeSeverity(config.formatSeverity);
|
|
129
|
+
if (err === "min") return normalizeSeverity(config.minSeverity);
|
|
130
|
+
return normalizeSeverity(config.maxSeverity);
|
|
131
|
+
}
|
|
132
|
+
function severityOfFormError(err, config) {
|
|
133
|
+
if (err === "order") return normalizeSeverity(config.orderSeverity);
|
|
134
|
+
return normalizeSeverity(config.rangeLimitSeverity);
|
|
135
|
+
}
|
|
114
136
|
function parseIso(s) {
|
|
115
137
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
116
138
|
if (!m) return null;
|
|
@@ -437,7 +459,13 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
437
459
|
"error-order",
|
|
438
460
|
"error-format",
|
|
439
461
|
"range-limit-message",
|
|
440
|
-
"range-limit-heading"
|
|
462
|
+
"range-limit-heading",
|
|
463
|
+
// BTN-10312 — per-error severity (default error = blocks Apply + red banner)
|
|
464
|
+
"error-min-severity",
|
|
465
|
+
"error-max-severity",
|
|
466
|
+
"error-format-severity",
|
|
467
|
+
"error-order-severity",
|
|
468
|
+
"range-limit-severity"
|
|
441
469
|
];
|
|
442
470
|
_rendered = false;
|
|
443
471
|
_today;
|
|
@@ -461,12 +489,22 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
461
489
|
_startInput = null;
|
|
462
490
|
_endInput = null;
|
|
463
491
|
_applyBtn = null;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Unified alert banner (BTN-10312). Shows AT MOST ONE error at a time. When
|
|
494
|
+
* multiple errors are simultaneously active, the banner reflects the
|
|
495
|
+
* most-recently-edited field (see `_lastEditedField`).
|
|
496
|
+
*/
|
|
497
|
+
_alertBannerEl = null;
|
|
468
498
|
_leftDayMap = /* @__PURE__ */ new Map();
|
|
469
499
|
_rightDayMap = /* @__PURE__ */ new Map();
|
|
500
|
+
/**
|
|
501
|
+
* BTN-10312 — tracks which field the user most-recently interacted with.
|
|
502
|
+
* When both `startError` and `endError` are active, the banner shows the
|
|
503
|
+
* error corresponding to this field. Set to `null` when no field has been
|
|
504
|
+
* touched (initial render, or external attribute set); the resolver falls
|
|
505
|
+
* back to a stable precedence in that case.
|
|
506
|
+
*/
|
|
507
|
+
_lastEditedField = null;
|
|
470
508
|
constructor() {
|
|
471
509
|
super();
|
|
472
510
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -501,10 +539,12 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
501
539
|
if (name === "start") {
|
|
502
540
|
this._start = val ?? "";
|
|
503
541
|
this._startRawInvalid = "";
|
|
542
|
+
this._lastEditedField = null;
|
|
504
543
|
}
|
|
505
544
|
if (name === "end") {
|
|
506
545
|
this._end = val ?? "";
|
|
507
546
|
this._endRawInvalid = "";
|
|
547
|
+
this._lastEditedField = null;
|
|
508
548
|
}
|
|
509
549
|
if (name === "preset") this._preset = val ?? "";
|
|
510
550
|
if (!this._rendered) return;
|
|
@@ -515,6 +555,30 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
515
555
|
}
|
|
516
556
|
this._syncValidity();
|
|
517
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* BTN-10312 — public reset of the picker's internal draft state to the
|
|
560
|
+
* currently committed `start` / `end` / `preset` attributes. Called by
|
|
561
|
+
* `bv-date-range-input` when the popup closes without Apply (Cancel,
|
|
562
|
+
* Escape, outside-click) so the next open shows the last-committed value
|
|
563
|
+
* cleanly, with no leftover error banners from the discarded edit.
|
|
564
|
+
*
|
|
565
|
+
* The external value is not modified — this only clears the draft.
|
|
566
|
+
*/
|
|
567
|
+
resetDraft() {
|
|
568
|
+
this._start = this.getAttribute("start") ?? "";
|
|
569
|
+
this._end = this.getAttribute("end") ?? "";
|
|
570
|
+
this._preset = this.getAttribute("preset") ?? "";
|
|
571
|
+
this._startRawInvalid = "";
|
|
572
|
+
this._endRawInvalid = "";
|
|
573
|
+
this._picking = false;
|
|
574
|
+
this._hover = "";
|
|
575
|
+
this._lastEditedField = null;
|
|
576
|
+
if (!this._rendered) return;
|
|
577
|
+
this._syncCalendars();
|
|
578
|
+
this._syncInputs();
|
|
579
|
+
this._syncPresets();
|
|
580
|
+
this._syncValidity();
|
|
581
|
+
}
|
|
518
582
|
_render() {
|
|
519
583
|
if (this._rendered) return;
|
|
520
584
|
this._rendered = true;
|
|
@@ -598,31 +662,11 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
598
662
|
this._applyBtn = applyBtn;
|
|
599
663
|
actions.append(cancelBtn, applyBtn);
|
|
600
664
|
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
665
|
const bannerWrap = document.createElement("div");
|
|
621
666
|
bannerWrap.className = "banner-wrap";
|
|
622
667
|
const banner = document.createElement("bv-alert-banner");
|
|
623
|
-
banner.setAttribute("variant", "warning");
|
|
624
668
|
banner.hidden = true;
|
|
625
|
-
this.
|
|
669
|
+
this._alertBannerEl = banner;
|
|
626
670
|
bannerWrap.appendChild(banner);
|
|
627
671
|
main.append(calsWrap, bannerWrap, footer);
|
|
628
672
|
picker.append(sidebar, main);
|
|
@@ -795,6 +839,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
795
839
|
this._preset = "custom";
|
|
796
840
|
this._picking = true;
|
|
797
841
|
this._hover = "";
|
|
842
|
+
this._lastEditedField = "start";
|
|
798
843
|
} else {
|
|
799
844
|
if (iso < this._start) {
|
|
800
845
|
this._end = this._start;
|
|
@@ -805,6 +850,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
805
850
|
this._picking = false;
|
|
806
851
|
this._hover = "";
|
|
807
852
|
this._preset = "custom";
|
|
853
|
+
this._lastEditedField = "end";
|
|
808
854
|
}
|
|
809
855
|
this._startRawInvalid = "";
|
|
810
856
|
this._endRawInvalid = "";
|
|
@@ -829,6 +875,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
829
875
|
this._hover = "";
|
|
830
876
|
this._startRawInvalid = "";
|
|
831
877
|
this._endRawInvalid = "";
|
|
878
|
+
this._lastEditedField = null;
|
|
832
879
|
if (start) {
|
|
833
880
|
const parts = start.split("-");
|
|
834
881
|
const y = parseInt(parts[0], 10);
|
|
@@ -844,6 +891,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
844
891
|
this._syncValidity();
|
|
845
892
|
}
|
|
846
893
|
_onInputBlur(input, field) {
|
|
894
|
+
this._lastEditedField = field;
|
|
847
895
|
const raw = input.value;
|
|
848
896
|
if (raw === "") {
|
|
849
897
|
if (field === "start") {
|
|
@@ -907,9 +955,9 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
907
955
|
},
|
|
908
956
|
cfg
|
|
909
957
|
);
|
|
910
|
-
this.
|
|
958
|
+
this._paintInputInvalidState();
|
|
959
|
+
this._applyAlertBanner(cfg);
|
|
911
960
|
this._applyApplyState();
|
|
912
|
-
this._applyBannerState();
|
|
913
961
|
this._applyCalendarDisabledState();
|
|
914
962
|
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
963
|
this.dispatchEvent(
|
|
@@ -931,88 +979,140 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
931
979
|
max,
|
|
932
980
|
maxRangeYears: Number.isFinite(maxRangeYears) ? maxRangeYears : void 0,
|
|
933
981
|
validateOrder: this.hasAttribute("validate-order"),
|
|
934
|
-
validateFormat: this.hasAttribute("validate-format")
|
|
982
|
+
validateFormat: this.hasAttribute("validate-format"),
|
|
983
|
+
minSeverity: normalizeSeverity(this.getAttribute("error-min-severity")),
|
|
984
|
+
maxSeverity: normalizeSeverity(this.getAttribute("error-max-severity")),
|
|
985
|
+
formatSeverity: normalizeSeverity(this.getAttribute("error-format-severity")),
|
|
986
|
+
orderSeverity: normalizeSeverity(this.getAttribute("error-order-severity")),
|
|
987
|
+
rangeLimitSeverity: normalizeSeverity(this.getAttribute("range-limit-severity"))
|
|
935
988
|
};
|
|
936
989
|
}
|
|
937
|
-
|
|
990
|
+
/**
|
|
991
|
+
* BTN-10312 — apply the `.invalid` + `aria-invalid` state to input fields
|
|
992
|
+
* whose field-level error is currently blocking (severity `'error'`).
|
|
993
|
+
* Non-blocking severities (warning/primary/success) do not paint the input
|
|
994
|
+
* red so the visual matches the semantic ("not a fatal error").
|
|
995
|
+
*/
|
|
996
|
+
_paintInputInvalidState() {
|
|
938
997
|
const v = this._validity;
|
|
939
|
-
this.
|
|
940
|
-
this.
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
998
|
+
const cfg = this._readConfig();
|
|
999
|
+
this._paintOne(this._startInput, v.startError ? severityOfFieldError(v.startError, cfg) : null);
|
|
1000
|
+
this._paintOne(this._endInput, v.endError ? severityOfFieldError(v.endError, cfg) : null);
|
|
1001
|
+
}
|
|
1002
|
+
_paintOne(input, sev) {
|
|
1003
|
+
if (!input) return;
|
|
1004
|
+
if (sev === "error") {
|
|
1005
|
+
input.classList.add("invalid");
|
|
1006
|
+
input.setAttribute("aria-invalid", "true");
|
|
1007
|
+
} else {
|
|
1008
|
+
input.classList.remove("invalid");
|
|
1009
|
+
input.removeAttribute("aria-invalid");
|
|
949
1010
|
}
|
|
950
1011
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1012
|
+
/**
|
|
1013
|
+
* BTN-10312 — single alert banner. Picks the "active" error following the
|
|
1014
|
+
* precedence:
|
|
1015
|
+
*
|
|
1016
|
+
* 1. If a form error is active, show it (form errors don't coexist with
|
|
1017
|
+
* field errors by validator design).
|
|
1018
|
+
* 2. If both field errors are active, show the one for
|
|
1019
|
+
* `_lastEditedField` (falling back to `start` if `null`).
|
|
1020
|
+
* 3. Else show whichever single field error is active.
|
|
1021
|
+
* 4. Else hide the banner.
|
|
1022
|
+
*
|
|
1023
|
+
* `show-range-limit-banner="false"` still suppresses the range-limit
|
|
1024
|
+
* banner specifically (existing consumer contract from BTN-10191).
|
|
1025
|
+
*/
|
|
1026
|
+
_applyAlertBanner(cfg) {
|
|
1027
|
+
if (!this._alertBannerEl) return;
|
|
1028
|
+
const pickerEl = this.shadowRoot.querySelector(".picker");
|
|
1029
|
+
const active = this._resolveActiveError();
|
|
1030
|
+
if (!active) {
|
|
1031
|
+
this._alertBannerEl.hidden = true;
|
|
1032
|
+
this._alertBannerEl.removeAttribute("heading");
|
|
1033
|
+
this._alertBannerEl.removeAttribute("message");
|
|
1034
|
+
pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
|
|
1035
|
+
return;
|
|
960
1036
|
}
|
|
961
|
-
if (
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
} else {
|
|
966
|
-
errorEl.textContent = "";
|
|
967
|
-
errorEl.hidden = true;
|
|
968
|
-
}
|
|
1037
|
+
if (active.kind === "form" && active.error === "range-limit" && this.getAttribute("show-range-limit-banner") === "false") {
|
|
1038
|
+
this._alertBannerEl.hidden = true;
|
|
1039
|
+
pickerEl == null ? void 0 : pickerEl.classList.remove("picker--with-banner");
|
|
1040
|
+
return;
|
|
969
1041
|
}
|
|
1042
|
+
const variant = active.kind === "field" ? severityOfFieldError(active.error, cfg) : severityOfFormError(active.error, cfg);
|
|
1043
|
+
this._alertBannerEl.setAttribute("variant", variant);
|
|
1044
|
+
if (active.kind === "form" && active.error === "range-limit") {
|
|
1045
|
+
const yearsAttr = this.getAttribute("max-range-years") ?? "";
|
|
1046
|
+
const heading = this.getAttribute("range-limit-heading") || (yearsAttr ? `Date range exceeds ${yearsAttr} years` : "Date range exceeds the maximum");
|
|
1047
|
+
const message = this.getAttribute("range-limit-message") || "Only CSV download is available for this date range. Live report generation is not supported.";
|
|
1048
|
+
this._alertBannerEl.setAttribute("heading", heading.replace(/\{N\}/g, yearsAttr));
|
|
1049
|
+
this._alertBannerEl.setAttribute("message", message.replace(/\{N\}/g, yearsAttr));
|
|
1050
|
+
} else {
|
|
1051
|
+
this._alertBannerEl.removeAttribute("heading");
|
|
1052
|
+
const message = active.kind === "field" ? this._resolveFieldError(active.error, active.field) : this._resolveOrderError();
|
|
1053
|
+
this._alertBannerEl.setAttribute("message", message);
|
|
1054
|
+
}
|
|
1055
|
+
this._alertBannerEl.hidden = false;
|
|
1056
|
+
pickerEl == null ? void 0 : pickerEl.classList.add("picker--with-banner");
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Pick which error the banner should reflect. See `_applyAlertBanner` for
|
|
1060
|
+
* the precedence rules.
|
|
1061
|
+
*/
|
|
1062
|
+
_resolveActiveError() {
|
|
1063
|
+
const v = this._validity;
|
|
1064
|
+
if (v.formError) return { kind: "form", error: v.formError };
|
|
1065
|
+
if (v.startError && v.endError) {
|
|
1066
|
+
const winner = this._lastEditedField ?? "start";
|
|
1067
|
+
return {
|
|
1068
|
+
kind: "field",
|
|
1069
|
+
error: winner === "start" ? v.startError : v.endError,
|
|
1070
|
+
field: winner
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
if (v.startError) return { kind: "field", error: v.startError, field: "start" };
|
|
1074
|
+
if (v.endError) return { kind: "field", error: v.endError, field: "end" };
|
|
1075
|
+
return null;
|
|
970
1076
|
}
|
|
971
1077
|
_resolveFieldError(err, field) {
|
|
972
1078
|
if (err === "format") {
|
|
973
|
-
|
|
1079
|
+
const custom2 = this.getAttribute("error-format");
|
|
1080
|
+
if (custom2) return this._interpolateField(custom2, field);
|
|
1081
|
+
return `Enter a valid ${field} date`;
|
|
974
1082
|
}
|
|
975
1083
|
if (err === "min") {
|
|
976
1084
|
const minIso = this.getAttribute("min") ?? "";
|
|
977
1085
|
const minDisplay = formatDisplay(minIso);
|
|
978
1086
|
const custom2 = this.getAttribute("error-min");
|
|
979
|
-
if (custom2) return
|
|
1087
|
+
if (custom2) return this._interpolateField(custom2, field).replace(/\{min\}/g, minDisplay);
|
|
980
1088
|
return `${capitalize(field)} date must be on or after ${minDisplay}`;
|
|
981
1089
|
}
|
|
982
1090
|
const maxAttr = this.getAttribute("max") ?? "";
|
|
983
1091
|
const resolvedMax = maxAttr === "today" ? isoToday() : maxAttr;
|
|
984
1092
|
const maxDisplay = formatDisplay(resolvedMax);
|
|
985
1093
|
const custom = this.getAttribute("error-max");
|
|
986
|
-
if (custom) return
|
|
1094
|
+
if (custom) return this._interpolateField(custom, field).replace(/\{max\}/g, maxDisplay);
|
|
987
1095
|
if (maxAttr === "today") return `${capitalize(field)} date cannot be in the future`;
|
|
988
1096
|
return `${capitalize(field)} date cannot be after ${maxDisplay}`;
|
|
989
1097
|
}
|
|
1098
|
+
/**
|
|
1099
|
+
* BTN-10312 — interpolate `{field}` (always lowercase) and `{Field}` (always
|
|
1100
|
+
* capitalized) in a user-provided template. Before this change, the historical
|
|
1101
|
+
* behavior was that `{field}` in custom copy resolved lowercase while the
|
|
1102
|
+
* built-in defaults capitalized inline — inconsistent. Now the split is
|
|
1103
|
+
* explicit: `{field}` = lowercase, `{Field}` = capitalized.
|
|
1104
|
+
*/
|
|
1105
|
+
_interpolateField(template, field) {
|
|
1106
|
+
return template.replace(/\{Field\}/g, capitalize(field)).replace(/\{field\}/g, field);
|
|
1107
|
+
}
|
|
990
1108
|
_resolveOrderError() {
|
|
991
|
-
return this.getAttribute("error-order")
|
|
1109
|
+
return this.getAttribute("error-order") || "Start date must be on or before end date";
|
|
992
1110
|
}
|
|
993
1111
|
_applyApplyState() {
|
|
994
1112
|
if (this._applyBtn) {
|
|
995
1113
|
this._applyBtn.disabled = !this._validity.applyEnabled;
|
|
996
1114
|
}
|
|
997
1115
|
}
|
|
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
1116
|
_applyCalendarDisabledState() {
|
|
1017
1117
|
const minIso = this.getAttribute("min") || "";
|
|
1018
1118
|
const maxAttr = this.getAttribute("max") || "";
|
|
@@ -1048,6 +1148,7 @@ var BvDateRangePicker = class extends HTMLElement {
|
|
|
1048
1148
|
var _a;
|
|
1049
1149
|
const ev = e;
|
|
1050
1150
|
if ((_a = ev.inputType) == null ? void 0 : _a.startsWith("delete")) return;
|
|
1151
|
+
this._lastEditedField = field;
|
|
1051
1152
|
const digits = input.value.replace(/[^\d]/g, "");
|
|
1052
1153
|
let formatted;
|
|
1053
1154
|
if (digits.length <= 2) {
|
|
@@ -1106,7 +1207,13 @@ var PICKER_PASSTHROUGH_ATTRS = [
|
|
|
1106
1207
|
"error-order",
|
|
1107
1208
|
"error-format",
|
|
1108
1209
|
"range-limit-message",
|
|
1109
|
-
"range-limit-heading"
|
|
1210
|
+
"range-limit-heading",
|
|
1211
|
+
// BTN-10312 — per-error severity attributes
|
|
1212
|
+
"error-min-severity",
|
|
1213
|
+
"error-max-severity",
|
|
1214
|
+
"error-format-severity",
|
|
1215
|
+
"error-order-severity",
|
|
1216
|
+
"range-limit-severity"
|
|
1110
1217
|
];
|
|
1111
1218
|
function isoToDisplay(iso) {
|
|
1112
1219
|
if (!iso) return "";
|
|
@@ -1120,6 +1227,8 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1120
1227
|
"end",
|
|
1121
1228
|
"placeholder",
|
|
1122
1229
|
"required",
|
|
1230
|
+
// BTN-10312 — local (not passed through to picker); gates popup close.
|
|
1231
|
+
"keep-open-while-invalid",
|
|
1123
1232
|
...PICKER_PASSTHROUGH_ATTRS
|
|
1124
1233
|
];
|
|
1125
1234
|
_rendered = false;
|
|
@@ -1128,6 +1237,12 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1128
1237
|
_triggerEl = null;
|
|
1129
1238
|
_pickerEl = null;
|
|
1130
1239
|
_dropdown = null;
|
|
1240
|
+
/**
|
|
1241
|
+
* BTN-10312 — cached validity from the internal picker. `canClose` on the
|
|
1242
|
+
* dropdown controller consults this to gate popup close when
|
|
1243
|
+
* `keep-open-while-invalid` is set.
|
|
1244
|
+
*/
|
|
1245
|
+
_pickerValidity = { valid: true, applyEnabled: false };
|
|
1131
1246
|
constructor() {
|
|
1132
1247
|
super();
|
|
1133
1248
|
adoptStyles(this.attachShadow({ mode: "open" }), bv_date_range_input_default);
|
|
@@ -1223,13 +1338,15 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1223
1338
|
});
|
|
1224
1339
|
picker.addEventListener("bv-validity-change", (e) => {
|
|
1225
1340
|
const detail = e.detail;
|
|
1341
|
+
this._pickerValidity = detail;
|
|
1342
|
+
const hasBlockingError = !detail.applyEnabled && !!(detail.startError || detail.endError || detail.formError);
|
|
1226
1343
|
if (this._triggerEl) {
|
|
1227
|
-
if (
|
|
1228
|
-
this._triggerEl.classList.remove("invalid");
|
|
1229
|
-
this._triggerEl.removeAttribute("aria-invalid");
|
|
1230
|
-
} else {
|
|
1344
|
+
if (hasBlockingError) {
|
|
1231
1345
|
this._triggerEl.classList.add("invalid");
|
|
1232
1346
|
this._triggerEl.setAttribute("aria-invalid", "true");
|
|
1347
|
+
} else {
|
|
1348
|
+
this._triggerEl.classList.remove("invalid");
|
|
1349
|
+
this._triggerEl.removeAttribute("aria-invalid");
|
|
1233
1350
|
}
|
|
1234
1351
|
}
|
|
1235
1352
|
this.dispatchEvent(
|
|
@@ -1251,6 +1368,28 @@ var BvDateRangeInput = class extends HTMLElement {
|
|
|
1251
1368
|
this._pickerEl.setAttribute("start", this.getAttribute("start") ?? "");
|
|
1252
1369
|
this._pickerEl.setAttribute("end", this.getAttribute("end") ?? "");
|
|
1253
1370
|
}
|
|
1371
|
+
},
|
|
1372
|
+
// BTN-10312 — gate popup close when the consumer opted in via
|
|
1373
|
+
// `keep-open-while-invalid` AND the picker has a blocking error.
|
|
1374
|
+
// Only *implicit* dismissals (Escape / outside-click) are gated —
|
|
1375
|
+
// explicit user actions (Cancel button, Apply, trigger toggle) always
|
|
1376
|
+
// close so the user can never get trapped in an invalid popup.
|
|
1377
|
+
// Non-blocking errors (warning/primary/success severity) do NOT block
|
|
1378
|
+
// close either: the value externally is still the last-committed one.
|
|
1379
|
+
canClose: (reason) => {
|
|
1380
|
+
if (reason === "programmatic") return true;
|
|
1381
|
+
if (!this.hasAttribute("keep-open-while-invalid")) return true;
|
|
1382
|
+
const v = this._pickerValidity;
|
|
1383
|
+
const hasBlockingError = !v.applyEnabled && !!(v.startError || v.endError || v.formError);
|
|
1384
|
+
return !hasBlockingError;
|
|
1385
|
+
},
|
|
1386
|
+
// BTN-10312 — after every successful close (including Apply, Cancel,
|
|
1387
|
+
// Escape, outside-click), reset the picker's internal draft state so the
|
|
1388
|
+
// next open shows the last-committed value cleanly. Apply cases end up
|
|
1389
|
+
// being a no-op because the draft already equals the committed value.
|
|
1390
|
+
onAfterClose: () => {
|
|
1391
|
+
var _a;
|
|
1392
|
+
(_a = this._pickerEl) == null ? void 0 : _a.resetDraft();
|
|
1254
1393
|
}
|
|
1255
1394
|
});
|
|
1256
1395
|
}
|