diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 92e5191fbf05..7c1d57141482 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -38,7 +38,7 @@ }, { "path": "./dist/js/bootstrap.bundle.min.js", - "maxSize": "55.5 kB" + "maxSize": "62.0 kB" }, { "path": "./dist/js/bootstrap.js", diff --git a/config.yml b/config.yml index 4032ed007f31..1a62f01797a5 100644 --- a/config.yml +++ b/config.yml @@ -36,7 +36,7 @@ cdn: js_bundle: "https://cdn.jsdelivr.net/npm/bootstrap@6.0.0-alpha1/dist/js/bootstrap.bundle.min.js" js_bundle_hash: "sha384-I2J4jlw924JZXHU9un9Mcuixq/rKhd5A8/B1NQ6ifPAiBFacZjwNcec8d6L38jQv" floating_ui_esm: "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.7.6/dist/floating-ui.dom.esm.min.js" - vanilla_calendar_pro_esm: "https://cdn.jsdelivr.net/npm/vanilla-calendar-pro@3.1.0/index.mjs" + vanilla_calendar_pro_esm: "https://cdn.jsdelivr.net/npm/vanilla-calendar-pro@3.3.1/index.mjs" anchors: min: 2 diff --git a/js/src/datepicker.ts b/js/src/datepicker.ts index b5eab5a65c58..f03a9c77f6fc 100644 --- a/js/src/datepicker.ts +++ b/js/src/datepicker.ts @@ -113,6 +113,7 @@ class Datepicker extends BaseComponent { protected declare _displayElement: HTMLElement | false | null protected declare _themeObserver: MutationObserver | null protected declare _onFocusIn: (event: Event) => void + protected declare _themeAncestor: Element | null | undefined constructor(element?: string | Element | null, config?: Partial | null) { super(element, config) @@ -304,7 +305,14 @@ class Datepicker extends BaseComponent { } protected _getThemeAncestor(): Element | null { - return this._element.closest('[data-bs-theme]') + // Cache on first lookup: for an inline calendar, `_syncThemeAttribute` writes + // `data-bs-theme` onto this same element (it doubles as VCP's main element), which + // would make a later `closest()` match itself instead of the real ancestor. + if (this._themeAncestor === undefined) { + this._themeAncestor = this._element.closest('[data-bs-theme]') + } + + return this._themeAncestor } protected _getEffectiveTheme(): string | null { @@ -391,7 +399,9 @@ class Datepicker extends BaseComponent { selectionDatesMode: this._config.selectionMode, selectedDates: this._config.selectedDates, displayMonthsCount: this._config.displayMonthsCount, - type: this._config.displayMonthsCount > 1 ? 'multiple' : 'default', + // Multiple months require VCP's 'multiple' type; otherwise let vcpOptions.type + // through (e.g. 'week') instead of always forcing the single-month default. + type: this._config.displayMonthsCount > 1 ? 'multiple' : (this._config.vcpOptions.type ?? 'default'), selectedTheme: vcpTheme, themeAttrDetect: '[data-bs-theme]', onClickDate: (self, event) => this._handleDateClick(self, event), diff --git a/js/tests/unit/datepicker.spec.js b/js/tests/unit/datepicker.spec.js index b0f669d981cd..aa375930eaf1 100644 --- a/js/tests/unit/datepicker.spec.js +++ b/js/tests/unit/datepicker.spec.js @@ -788,6 +788,62 @@ describe('Datepicker', () => { }) }) + // These assertions pin the Vanilla Calendar Pro markup that our Sass targets. + // VCP 3.2.0 added the row and cell wrappers for ARIA, which moved the grid + // columns off the container and broke the month and year layout silently. + describe('calendar markup', () => { + const createInlineCalendar = () => { + fixtureEl.innerHTML = '
' + + const divEl = fixtureEl.querySelector('div') + Datepicker.getOrCreateInstance(divEl) + + return divEl + } + + it('should wrap months in rows and cells', () => { + const divEl = createInlineCalendar() + + divEl.querySelector('[data-vc="month"]').click() + + const monthsEl = divEl.querySelector('[data-vc="months"]') + + expect(monthsEl.querySelectorAll('[data-vc-months="row"]')).toHaveSize(3) + expect(monthsEl.querySelectorAll('[data-vc-months="row"] > [data-vc-months="cell"] > [data-vc-months-month]')).toHaveSize(12) + }) + + it('should wrap years in rows and cells', () => { + const divEl = createInlineCalendar() + + divEl.querySelector('[data-vc="year"]').click() + + const yearsEl = divEl.querySelector('[data-vc="years"]') + + expect(yearsEl.querySelectorAll('[data-vc-years="row"]')).toHaveSize(3) + expect(yearsEl.querySelectorAll('[data-vc-years="row"] > [data-vc-years="cell"] > [data-vc-years-year]')).toHaveSize(15) + }) + + it('should not mark inline calendars with data-vc-input', () => { + const divEl = createInlineCalendar() + + expect(divEl.getAttribute('data-vc')).toEqual('calendar') + expect(divEl.hasAttribute('data-vc-input')).toBeFalse() + }) + + it('should mark popup calendars with data-vc-input', () => { + fixtureEl.innerHTML = '' + + const inputEl = fixtureEl.querySelector('input') + const datepicker = new Datepicker(inputEl) + + return datepicker.show().then(() => { + const calendarEl = datepicker._calendar.context.mainElement + + expect(calendarEl.hasAttribute('data-vc-input')).toBeTrue() + }) + }) + }) + describe('data-api', () => { it('should toggle on click for buttons', () => { return new Promise(resolve => { @@ -1076,6 +1132,42 @@ describe('Datepicker', () => { expect(datepicker._config.vcpOptions.jumpMonths).toEqual(2) }) + + it('should default to the single-month calendar type', () => { + fixtureEl.innerHTML = '' + + const inputEl = fixtureEl.querySelector('input') + const datepicker = new Datepicker(inputEl) + + expect(datepicker._buildCalendarOptions().type).toEqual('default') + }) + + it('should let vcpOptions.type override the default calendar type', () => { + fixtureEl.innerHTML = '' + + const inputEl = fixtureEl.querySelector('input') + const datepicker = new Datepicker(inputEl, { + vcpOptions: { + type: 'week' + } + }) + + expect(datepicker._buildCalendarOptions().type).toEqual('week') + }) + + it('should force the multiple calendar type when displayMonthsCount is greater than 1, ignoring vcpOptions.type', () => { + fixtureEl.innerHTML = '' + + const inputEl = fixtureEl.querySelector('input') + const datepicker = new Datepicker(inputEl, { + displayMonthsCount: 2, + vcpOptions: { + type: 'week' + } + }) + + expect(datepicker._buildCalendarOptions().type).toEqual('multiple') + }) }) describe('date selection handling', () => { diff --git a/js/tests/visual/datepicker.html b/js/tests/visual/datepicker.html index b2c76dac9502..fbd2bdfa973a 100644 --- a/js/tests/visual/datepicker.html +++ b/js/tests/visual/datepicker.html @@ -72,6 +72,24 @@

Sunday First


+

Inline

+
+
+

No popup surface. The calendar inherits the page background.

+
+
+
+

Inline inside a card, range selection.

+
+
+
+
+
+
+
+ +
+

Dark Mode

diff --git a/package-lock.json b/package-lock.json index 12b8bb05de6e..2bec35b65a67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ }, "peerDependencies": { "@floating-ui/dom": "^1.7.6", - "vanilla-calendar-pro": "^3.1.0" + "vanilla-calendar-pro": "^3.3.1" } }, "node_modules/@astrojs/check": { @@ -18532,9 +18532,9 @@ } }, "node_modules/vanilla-calendar-pro": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vanilla-calendar-pro/-/vanilla-calendar-pro-3.1.0.tgz", - "integrity": "sha512-yXDtCaedcKz6i5OOdWGwui0C8MAmjXjj7JzKZyjDlkczSRqnhI8BDGFygqT2K+qL1uY7R2fLYlTlxA6oyFs2yg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/vanilla-calendar-pro/-/vanilla-calendar-pro-3.3.1.tgz", + "integrity": "sha512-3+PCVaaZoF03+D9oXzPQwEMu5DeAlllSOfpSmzojGZSLFb9OJYK4wMtMlPDNOe59DyEy8CGrc3Aw9TvlAnoANg==", "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index 7634c9957e2a..6d810cb04778 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ }, "peerDependencies": { "@floating-ui/dom": "^1.7.6", - "vanilla-calendar-pro": "^3.1.0" + "vanilla-calendar-pro": "^3.3.1" }, "devDependencies": { "@astrojs/check": "^0.9.10", diff --git a/scss/_calendar.scss b/scss/_calendar.scss new file mode 100644 index 000000000000..908174eb9be2 --- /dev/null +++ b/scss/_calendar.scss @@ -0,0 +1,417 @@ +// stylelint-disable selector-max-attribute, property-disallowed-list, selector-no-qualifying-type -- VCP uses extensive data attributes and requires direct border-radius properties for range selection + +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/mask-icon" as *; +@use "mixins/tokens" as *; + +$calendar-tokens: () !default; + +// scss-docs-start calendar-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$calendar-tokens: defaults( + ( + --calendar-padding: 0, + --calendar-color: var(--fg-body), + --calendar-font-size: var(--font-size-sm), + --calendar-header-font-weight: 600, + --calendar-weekday-color: var(--fg-3), + --calendar-day-hover-bg: var(--bg-1), + --calendar-day-selected-bg: var(--primary-bg), + --calendar-day-selected-color: var(--primary-contrast), + --calendar-day-today-bg: var(--bg-2), + --calendar-day-today-color: var(--fg-1), + --calendar-day-disabled-color: var(--fg-4), + ), + $calendar-tokens +); +// scss-docs-end calendar-tokens + +@layer components { + // The calendar renders unadorned by default. The datepicker layers the popup + // surface on top for the `[data-vc-input]` variant. + [data-vc="calendar"] { + @include tokens($calendar-tokens); + + position: relative; + box-sizing: border-box; + display: flex; + flex-direction: column; + width: fit-content; + padding: var(--calendar-padding); + font-family: var(--body-font-family); + font-size: var(--calendar-font-size); + color: var(--calendar-color); + color-scheme: light dark; + + // Respond to Bootstrap's color mode system + &[data-bs-theme="light"] { + color-scheme: light; + } + + &[data-bs-theme="dark"] { + color-scheme: dark; + } + + // Catch-all for focus styles + button:focus-visible { + position: relative; + z-index: 1; + @include focus-ring(); + } + } + + [data-vc-arrow] { + position: relative; + display: block; + width: 2rem; + height: 2rem; + color: var(--calendar-color); + pointer-events: auto; + cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &::before { + position: absolute; + inset: .25rem; + content: ""; + background-color: var(--calendar-color); + @include mask-icon(url("data:image/svg+xml,"), $size: null); + } + + &:hover { + background-color: var(--calendar-day-hover-bg); + } + } + + [data-vc-arrow="prev"]::before { + transform: rotate(90deg); + } + + [data-vc-arrow="next"]::before { + transform: rotate(-90deg); + } + + // Grid layout + // + // The controls overlay the header, so they repeat the calendar's own inset to + // keep the arrows aligned with the content below. + [data-vc="controls"] { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; + padding-block-start: var(--calendar-padding); + padding-inline: var(--calendar-padding); + pointer-events: none; + } + + [data-vc="grid"] { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + gap: 1.75rem; + } + + [data-vc="column"] { + display: flex; + flex-grow: 1; + flex-direction: column; + min-width: 240px; + } + + // Animated transitions + // + // With the `animation` option on, VCP clips the outgoing view (`data-vc-clip`) + // and layers a `data-vc-ghost` clone of it on top while the incoming view + // animates in. Without the clip and the absolute positioning, the outgoing + // clone stays in normal flow and shows through underneath the calendar. + [data-vc-clip] { + position: relative; + overflow: clip; + } + + [data-vc-ghost] { + position: absolute; + overflow: clip; + pointer-events: none; + } + + [data-vc-collapsing] { + flex-shrink: 0; + align-content: start; + overflow: clip; + } + + // + // Header + // + + [data-vc="header"] { + position: relative; + display: flex; + align-items: center; + margin-bottom: .75rem; + } + + // Month and year + [data-vc-header="content"] { + display: inline-flex; + flex-grow: 1; + align-items: center; + justify-content: center; + white-space: pre-wrap; + } + + [data-vc="month"], + [data-vc="year"] { + padding: .25rem .5rem; + margin-inline: -.125rem; + font-size: 1rem; + font-weight: var(--calendar-header-font-weight); + color: var(--calendar-color); + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &:disabled { + color: var(--calendar-day-disabled-color); + pointer-events: none; + } + + &:hover:not(:disabled) { + background-color: var(--calendar-day-hover-bg); + } + } + + [data-vc="content"] { + display: flex; + flex-grow: 1; + flex-direction: column; + } + + // Month/Year grids + // + // VCP wraps each set of months and years in role="row" and role="gridcell" + // elements, so the column grid goes on the row and not on the container. + [data-vc="months"], + [data-vc="years"] { + display: flex; + flex-grow: 1; + flex-direction: column; + row-gap: 1rem; + } + + [data-vc-months="row"], + [data-vc-years="row"] { + display: grid; + grid-template-columns: repeat(var(--calendar-columns, 4), minmax(0, 1fr)); + column-gap: .25rem; + align-items: center; + } + + [data-vc="years"] { + --calendar-columns: 5; + } + + [data-vc-months="cell"], + [data-vc-years="cell"] { + display: flex; + } + + [data-vc-months-month], + [data-vc-years-year] { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 2.5rem; + padding: .25rem; + font-size: .75rem; + font-weight: 600; + line-height: 1rem; + color: var(--calendar-weekday-color); + text-align: center; + word-break: break-all; + cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &:disabled { + color: var(--calendar-day-disabled-color); + pointer-events: none; + } + + &:hover:not(:disabled) { + background-color: var(--calendar-day-hover-bg); + } + + &[data-vc-months-month-selected], + &[data-vc-years-year-selected] { + color: var(--calendar-day-selected-color); + background-color: var(--calendar-day-selected-bg); + + &:hover { + color: var(--calendar-day-selected-color); + background-color: var(--calendar-day-selected-bg); + } + } + } + + // Week days header + [data-vc="week"] { + display: grid; + grid-template-columns: repeat(7, 1fr); + justify-items: center; + margin-bottom: .5rem; + } + + [data-vc-week-day] { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-width: 1.875rem; + padding: 0; + margin: 0; + font-size: .75rem; + font-weight: 600; + line-height: 1rem; + color: var(--calendar-weekday-color); + background-color: transparent; + border: 0; + } + + button[data-vc-week-day] { + cursor: pointer; + } + + // Dates grid + [data-vc="dates"] { + pointer-events: none; + } + + [data-vc-dates="row"] { + display: grid; + grid-template-columns: repeat(7, 1fr); + align-items: center; + justify-items: center; + width: 100%; + } + + [data-vc-date] { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding-top: .125rem; + padding-bottom: .125rem; + pointer-events: auto; + + &:not(:has([data-vc-date-btn])), + &[data-vc-date-disabled], + &[data-vc-date-disabled] [data-vc-date-btn] { + pointer-events: none; + } + } + + // Date button + [data-vc-date-btn] { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-width: 1.875rem; + height: 100%; + min-height: 1.875rem; + padding: 0; + font-size: .75rem; + font-weight: 400; + line-height: 1rem; + color: var(--calendar-color); + cursor: pointer; + background-color: transparent; + border: 0; + border-radius: var(--radius-5); + + &:hover { + background-color: var(--calendar-day-hover-bg); + } + } + + // Today + [data-vc-date-today] [data-vc-date-btn] { + font-weight: 600; + color: var(--calendar-day-today-color); + background-color: var(--calendar-day-today-bg); + } + + // Outside month + [data-vc-date-month="next"] [data-vc-date-btn], + [data-vc-date-month="prev"] [data-vc-date-btn] { + opacity: .5; + } + + // Disabled + [data-vc-date-disabled] [data-vc-date-btn] { + color: var(--calendar-day-disabled-color); + } + + // Range selection styles + [data-vc-date-hover] [data-vc-date-btn] { + background-color: var(--calendar-day-hover-bg); + border-radius: 0; + } + + [data-vc-date-hover="first"] [data-vc-date-btn] { + border-start-start-radius: var(--radius-5); + border-end-start-radius: var(--radius-5); + } + + [data-vc-date-hover="last"] [data-vc-date-btn] { + border-start-end-radius: var(--radius-5); + border-end-end-radius: var(--radius-5); + } + + [data-vc-date-hover="first-and-last"] [data-vc-date-btn] { + border-radius: var(--radius-5); + } + + [data-vc-date-selected="middle"] [data-vc-date-btn] { + border-radius: 0; + opacity: .8; + } + + // Selected + [data-vc-date-selected] [data-vc-date-btn] { + color: var(--calendar-day-selected-color); + background-color: var(--calendar-day-selected-bg); + } + + [data-vc-date-selected="first"] [data-vc-date-btn] { + border-top-left-radius: var(--radius-5); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: var(--radius-5); + } + + [data-vc-date-selected="last"] [data-vc-date-btn] { + border-top-left-radius: 0; + border-top-right-radius: var(--radius-5); + border-bottom-right-radius: var(--radius-5); + border-bottom-left-radius: 0; + } + + [data-vc-date-selected="first-and-last"] [data-vc-date-btn] { + border-radius: var(--radius-5); + } +} diff --git a/scss/_datepicker.scss b/scss/_datepicker.scss index e4578ce6cfdb..ebe21268ec67 100644 --- a/scss/_datepicker.scss +++ b/scss/_datepicker.scss @@ -1,10 +1,5 @@ -// stylelint-disable selector-max-attribute, property-disallowed-list, selector-no-qualifying-type -- VCP uses extensive data attributes and requires direct border-radius properties for range selection - @use "functions" as *; -@use "config" as *; @use "mixins/border-radius" as *; -@use "mixins/focus-ring" as *; -@use "mixins/mask-icon" as *; @use "mixins/tokens" as *; $datepicker-tokens: () !default; @@ -15,63 +10,33 @@ $datepicker-tokens: defaults( ( --datepicker-padding: 1rem, --datepicker-bg: var(--bg-body), - --datepicker-color: var(--fg-body), --datepicker-border-color: var(--border-color-translucent), --datepicker-border-width: var(--border-width), --datepicker-border-radius: var(--radius-7), --datepicker-box-shadow: var(--box-shadow), - --datepicker-font-size: var(--font-size-sm), --datepicker-min-width: 280px, --datepicker-zindex: var(--z-menu), - --datepicker-header-font-weight: 600, - --datepicker-weekday-color: var(--fg-3), - --datepicker-day-hover-bg: var(--bg-1), - --datepicker-day-selected-bg: var(--primary-bg), - --datepicker-day-selected-color: var(--primary-contrast), - --datepicker-day-today-bg: var(--bg-2), - --datepicker-day-today-color: var(--fg-1), - --datepicker-day-disabled-color: var(--fg-4), ), $datepicker-tokens ); // scss-docs-end datepicker-tokens @layer components { - [data-vc="calendar"] { + // The popup surface. VCP only adds `[data-vc-input]` when it owns a popup, so + // inline calendars keep the bare styling from the calendar component. + [data-vc="calendar"][data-vc-input] { @include tokens($datepicker-tokens); + --calendar-padding: var(--datepicker-padding); + position: absolute; z-index: var(--datepicker-zindex); - box-sizing: border-box; - display: flex; - flex-direction: column; min-width: var(--datepicker-min-width); - padding: var(--datepicker-padding); - font-family: var(--body-font-family); - font-size: var(--datepicker-font-size); - color: var(--datepicker-color); - color-scheme: light dark; background-color: var(--datepicker-bg); border: var(--datepicker-border-width) solid var(--datepicker-border-color); box-shadow: var(--datepicker-box-shadow); opacity: 1; @include border-radius(var(--datepicker-border-radius)); - - // Respond to Bootstrap's color mode system - &[data-bs-theme="light"] { - color-scheme: light; - } - - &[data-bs-theme="dark"] { - color-scheme: dark; - } - - // Catch-all for focus styles - button:focus-visible { - position: relative; - z-index: 1; - @include focus-ring(); - } } [data-vc-calendar-hidden] { @@ -79,17 +44,6 @@ $datepicker-tokens: defaults( opacity: 0; } - // Inline calendars - // - // Remove popover styling for more neutral styling - [data-vc="calendar"]:not([data-vc-input]) { - position: relative; - width: fit-content; - padding: 0; - border: 0; - box-shadow: none; - } - [data-vc-position="bottom"] { margin-block-start: .25rem; } @@ -97,319 +51,4 @@ $datepicker-tokens: defaults( [data-vc-position="top"] { margin-block-end: -.25rem; } - - [data-vc-arrow] { - position: relative; - display: block; - width: 2rem; - height: 2rem; - color: var(--datepicker-color); - pointer-events: auto; - cursor: pointer; - background-color: transparent; - border: 0; - @include border-radius(var(--radius-5)); - - &::before { - position: absolute; - inset: .25rem; - content: ""; - background-color: var(--datepicker-color); - @include mask-icon(url("data:image/svg+xml,"), $size: null); - } - - &:hover { - background-color: var(--datepicker-day-hover-bg); - } - } - - [data-vc-arrow="prev"]::before { - transform: rotate(90deg); - } - - [data-vc-arrow="next"]::before { - transform: rotate(-90deg); - } - - // Grid layout - [data-vc="controls"] { - position: absolute; - top: 0; - right: 0; - left: 0; - z-index: 20; - display: flex; - align-items: center; - justify-content: space-between; - padding-top: 1rem; - padding-right: 1rem; - padding-left: 1rem; - pointer-events: none; - } - - [data-vc="grid"] { - display: flex; - flex-grow: 1; - flex-wrap: wrap; - gap: 1.75rem; - } - - [data-vc="column"] { - display: flex; - flex-grow: 1; - flex-direction: column; - min-width: 240px; - } - - // - // Header - // - - [data-vc="header"] { - position: relative; - display: flex; - align-items: center; - margin-bottom: .75rem; - } - - // Month and year - [data-vc-header="content"] { - display: inline-flex; - flex-grow: 1; - align-items: center; - justify-content: center; - white-space: pre-wrap; - } - - [data-vc="month"], - [data-vc="year"] { - padding: .25rem .5rem; - margin-inline: -.125rem; - font-size: 1rem; - font-weight: var(--datepicker-header-font-weight); - color: var(--datepicker-color); - // cursor: pointer; - background-color: transparent; - border: 0; - @include border-radius(var(--radius-5)); - - &:disabled { - color: var(--datepicker-day-disabled-color); - pointer-events: none; - } - - &:hover:not(:disabled) { - background-color: var(--datepicker-day-hover-bg); - } - } - - [data-vc="content"] { - display: flex; - flex-grow: 1; - flex-direction: column; - } - - // Month/Year grids - [data-vc="months"], - [data-vc="years"] { - display: grid; - flex-grow: 1; - grid-template-columns: repeat(var(--vc-columns, 4), minmax(0, 1fr)); - row-gap: 1rem; - column-gap: .25rem; - align-items: center; - } - - [data-vc="years"] { - --vc-columns: 5; - } - - [data-vc-months-month], - [data-vc-years-year] { - display: flex; - align-items: center; - justify-content: center; - height: 2.5rem; - padding: .25rem; - font-size: .75rem; - font-weight: 600; - line-height: 1rem; - color: var(--datepicker-weekday-color); - text-align: center; - word-break: break-all; - cursor: pointer; - background-color: transparent; - border: 0; - @include border-radius(var(--radius-5)); - - &:disabled { - color: var(--datepicker-day-disabled-color); - pointer-events: none; - } - - &:hover:not(:disabled) { - background-color: var(--datepicker-day-hover-bg); - } - - &[data-vc-months-month-selected], - &[data-vc-years-year-selected] { - color: var(--datepicker-day-selected-color); - background-color: var(--datepicker-day-selected-bg); - - &:hover { - color: var(--datepicker-day-selected-color); - background-color: var(--datepicker-day-selected-bg); - } - } - } - - // Week days header - [data-vc="week"] { - display: grid; - grid-template-columns: repeat(7, 1fr); - justify-items: center; - margin-bottom: .5rem; - } - - [data-vc-week-day] { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - min-width: 1.875rem; - padding: 0; - margin: 0; - font-size: .75rem; - font-weight: 600; - line-height: 1rem; - color: var(--datepicker-weekday-color); - background-color: transparent; - border: 0; - } - - button[data-vc-week-day] { - cursor: pointer; - } - - // Dates grid - [data-vc="dates"] { - pointer-events: none; - } - - [data-vc-dates="row"] { - display: grid; - grid-template-columns: repeat(7, 1fr); - align-items: center; - justify-items: center; - width: 100%; - } - - [data-vc-date] { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - padding-top: .125rem; - padding-bottom: .125rem; - pointer-events: auto; - - &:not(:has([data-vc-date-btn])), - &[data-vc-date-disabled], - &[data-vc-date-disabled] [data-vc-date-btn] { - pointer-events: none; - } - } - - // Date button - [data-vc-date-btn] { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - min-width: 1.875rem; - height: 100%; - min-height: 1.875rem; - padding: 0; - font-size: .75rem; - font-weight: 400; - line-height: 1rem; - color: var(--datepicker-color); - cursor: pointer; - background-color: transparent; - border: 0; - border-radius: var(--radius-5); - - &:hover { - background-color: var(--datepicker-day-hover-bg); - } - } - - // Today - [data-vc-date-today] [data-vc-date-btn] { - font-weight: 600; - color: var(--datepicker-day-today-color); - background-color: var(--datepicker-day-today-bg); - } - - // Outside month - [data-vc-date-month="next"] [data-vc-date-btn], - [data-vc-date-month="prev"] [data-vc-date-btn] { - opacity: .5; - } - - // Disabled - [data-vc-date-disabled] [data-vc-date-btn] { - color: var(--datepicker-day-disabled-color); - } - - // Range selection styles - [data-vc-date-hover] [data-vc-date-btn] { - background-color: var(--datepicker-day-hover-bg); - border-radius: 0; - } - - [data-vc-date-hover="first"] [data-vc-date-btn] { - border-start-start-radius: var(--radius-5); - border-end-start-radius: var(--radius-5); - } - - [data-vc-date-hover="last"] [data-vc-date-btn] { - border-start-end-radius: var(--radius-5); - border-end-end-radius: var(--radius-5); - } - - [data-vc-date-hover="first-and-last"] [data-vc-date-btn] { - border-radius: var(--radius-5); - } - - [data-vc-date-selected="middle"] [data-vc-date-btn] { - border-radius: 0; - opacity: .8; - } - - // Selected - [data-vc-date-selected] [data-vc-date-btn] { - color: var(--datepicker-day-selected-color); - background-color: var(--datepicker-day-selected-bg); - - } - - [data-vc-date-selected="first"] [data-vc-date-btn] { - border-top-left-radius: var(--radius-5); - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: var(--radius-5); - } - - [data-vc-date-selected="last"] [data-vc-date-btn] { - border-top-left-radius: 0; - border-top-right-radius: var(--radius-5); - border-bottom-right-radius: var(--radius-5); - border-bottom-left-radius: 0; - } - - [data-vc-date-selected="first-and-last"] [data-vc-date-btn] { - border-radius: var(--radius-5); - } } diff --git a/scss/bootstrap.scss b/scss/bootstrap.scss index 58960c6e9a10..d914cc470a48 100644 --- a/scss/bootstrap.scss +++ b/scss/bootstrap.scss @@ -26,6 +26,7 @@ @forward "avatar"; @forward "badge"; @forward "breadcrumb"; +@forward "calendar"; @forward "chip"; @forward "card"; @forward "carousel"; diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index ff804192b41f..c91b735d3b4a 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -127,6 +127,9 @@ - title: Breadcrumb - title: Button - title: Button group + - title: Calendar + meta: + - added: 6.0.0 - title: Card - title: Carousel - title: Close button diff --git a/site/src/content/docs/components/calendar.mdx b/site/src/content/docs/components/calendar.mdx new file mode 100644 index 000000000000..5a80210a97b5 --- /dev/null +++ b/site/src/content/docs/components/calendar.mdx @@ -0,0 +1,98 @@ +--- +title: Calendar +description: The date grid that powers the datepicker. Render it inline for a standalone calendar, and style it with its own set of tokens. +toc: true +css_layer: components +js: required +deps: + - title: Datepicker + - title: Vanilla Calendar Pro + url: https://vanilla-calendar.pro/ +--- + +## How it works + +The calendar is the date grid: the header, the weekdays, the dates, and the month and year views. The [datepicker]([[docsref:/forms/datepicker]]) adds the popup surface around it and binds it to a form control. + +- [Vanilla Calendar Pro](https://vanilla-calendar.pro/) builds the markup. Bootstrap styles the `data-vc-*` attributes it writes. +- Bootstrap has no separate `Calendar` class. Render a calendar with the datepicker’s `inline` option. +- A calendar renders unadorned. It has no background, border, or padding, so it sits in your own container. +- The datepicker adds the popup surface only when it owns a popup. Style the two parts apart with `--calendar-*` and `--datepicker-*` tokens. + +Add `data-bs-inline="true"` to render a calendar instead of a popup. Bootstrap initializes inline calendars on page load, so they need no extra JavaScript. + +
`} /> + +Point at the month or the year in the header to switch to the month or year view. + +## Examples + +### In a card + +A calendar inherits the page background and adds no padding of its own. Put it in a [card]([[docsref:/components/card]]) to give it a surface. + + +
+
+
+
`} /> + +### Range selection + +Add `data-bs-selection-mode="multiple-ranged"` to select a start date and an end date. + +`} /> + +### Multiple dates + +Add `data-bs-selection-mode="multiple"` to select any number of dates. + +`} /> + +### Multiple months + +Add `data-bs-display-months-count` to show more than one month side by side. + +`} /> + +### Form binding + +Put a hidden input inside the calendar to submit the selection with a form. Bootstrap writes the selected dates to it in `YYYY-MM-DD` format, separated by commas. + + +
+ +
+ + `} /> + +## Color modes + +The calendar follows Bootstrap’s color modes. Set `data-bs-theme` on any ancestor and the calendar picks it up. + + +
+ `} /> + +## Accessibility + +Vanilla Calendar Pro marks up the grid with `role="grid"`, `role="row"`, and `role="gridcell"`, and it labels each date. The calendar itself carries `role="group"` (`role="dialog"` for a popup). The date grid is a single tab stop: arrow keys move focus between dates instead of tabbing through each one. Give the calendar an accessible name when the surrounding content does not already name it. + +## Usage + +The calendar has no JavaScript of its own. Use the [datepicker]([[docsref:/forms/datepicker]]) for options, methods, and events, and pass `inline: true` to render a calendar. + +```js +const calendar = new bootstrap.Datepicker(element, { + inline: true, + selectionMode: 'multiple-ranged' +}) +``` + +## CSS + +### Variables + +The calendar owns the tokens for the grid. The datepicker owns the tokens for the popup surface, such as its background, border, and shadow. Override the `--calendar-*` tokens to restyle a calendar in both places at once. + + diff --git a/site/src/content/docs/forms/datepicker.mdx b/site/src/content/docs/forms/datepicker.mdx index 1c09a97b20a6..60ae13bb0dbf 100644 --- a/site/src/content/docs/forms/datepicker.mdx +++ b/site/src/content/docs/forms/datepicker.mdx @@ -89,6 +89,30 @@ For selecting date ranges that span multiple months, combine `data-bs-selection- Select date range `} /> +### Week view + +Show a single week instead of a full month. Pass `type: 'week'` through `vcpOptions`. The picker steps week by week. + +`} /> + +### Animated transitions + +Animate paging between months and switching views. Pass `animation: true` through `vcpOptions`. Animation is off by default, and it respects `prefers-reduced-motion`. + +`} /> + +### Swipe navigation + +Page through months, weeks, or years with a mouse, touch, or pen. Pass `enableSwipe: true` through `vcpOptions`. + +`} /> + +### Collapse to a week + +Fold a month down to the week that holds the selected date. Drag the grabber, or click it, to expand the month back. Pass `enableCollapse: true` through `vcpOptions`. + +`} /> + ## Options ### First day of week @@ -205,10 +229,32 @@ Use `data-bs-datepicker-theme` to set the datepicker popup’s theme independent Light input, dark datepicker `} /> +## Shadow DOM + +The calendar renders inside a shadow root, which is useful for encapsulated web components. Two things differ from normal use: + +- Initialize the datepicker with JavaScript. Delegated events change their target at the shadow boundary, so `data-bs-toggle="datepicker"` does not reach elements inside a shadow root. +- Adopt Bootstrap’s stylesheet into the shadow root. Document styles do not cross the boundary, so the calendar renders unstyled without it. + +```js +const sheet = new CSSStyleSheet() +sheet.replaceSync(await (await fetch('/path/to/bootstrap.min.css')).text()) + +const root = host.attachShadow({ mode: 'open' }) +root.adoptedStyleSheets = [sheet] +root.innerHTML = '' + +new bootstrap.Datepicker(root.getElementById('shadowDatepicker')) +``` + ## CSS ### Variables +The calendar grid and the popup surface use separate token maps. Style an inline calendar with the `--calendar-*` tokens. The `--datepicker-*` tokens only apply to the popup that the datepicker adds. + + + ## Usage @@ -283,6 +329,8 @@ const datepicker = new bootstrap.Datepicker(element, { See the [Vanilla Calendar Pro documentation](https://vanilla-calendar.pro/docs/reference/settings) for all available options. +`vcpOptions.type` sets the calendar view (for example `'week'`, shown above). `displayMonthsCount` takes priority: set it above `1` and the datepicker forces the `'multiple'` type, overriding `vcpOptions.type`. + ### Methods