From: Mark Otto Date: Fri, 23 Jan 2026 16:17:13 +0000 (-0800) Subject: Massive update X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=485e22b68a39bf906663708c8bfe15d46cdf39c4;p=thirdparty%2Fbootstrap.git Massive update --- diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 86955dd597..5bd5323aaa 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -102,6 +102,7 @@ const Default = { floatingConfig: null, placement: DEFAULT_PLACEMENT, reference: 'toggle', + strategy: 'absolute', // Submenu options submenuTrigger: 'both', // 'click', 'hover', or 'both' submenuDelay: SUBMENU_CLOSE_DELAY @@ -115,6 +116,7 @@ const DefaultType = { floatingConfig: '(null|object|function)', placement: 'string', reference: '(string|element|object)', + strategy: 'string', submenuTrigger: 'string', submenuDelay: 'number' } @@ -326,7 +328,8 @@ class Dropdown extends BaseComponent { referenceElement, this._menu, floatingConfig.placement, - floatingConfig.middleware + floatingConfig.middleware, + floatingConfig.strategy ) } @@ -434,7 +437,8 @@ class Dropdown extends BaseComponent { _getFloatingConfig(placement, middleware) { const defaultConfig = { placement, - middleware + middleware, + strategy: this._config.strategy } return { @@ -451,7 +455,7 @@ class Dropdown extends BaseComponent { } // Shared helper for positioning any floating element - async _applyFloatingPosition(reference, floating, placement, middleware) { + async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') { if (!floating.isConnected) { return null } @@ -459,7 +463,7 @@ class Dropdown extends BaseComponent { const { x, y, placement: finalPlacement } = await computePosition( reference, floating, - { placement, middleware } + { placement, middleware, strategy } ) if (!floating.isConnected) { @@ -467,7 +471,7 @@ class Dropdown extends BaseComponent { } Object.assign(floating.style, { - position: 'absolute', + position: strategy, left: `${x}px`, top: `${y}px`, margin: '0' diff --git a/js/src/nav-overflow.js b/js/src/nav-overflow.js new file mode 100644 index 0000000000..c723371d8a --- /dev/null +++ b/js/src/nav-overflow.js @@ -0,0 +1,290 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap nav-overflow.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +import BaseComponent from './base-component.js' +import EventHandler from './dom/event-handler.js' +import SelectorEngine from './dom/selector-engine.js' +import Dropdown from './dropdown.js' + +/** + * Constants + */ + +const NAME = 'navoverflow' +const DATA_KEY = 'bs.navoverflow' +const EVENT_KEY = `.${DATA_KEY}` + +const EVENT_UPDATE = `update${EVENT_KEY}` +const EVENT_OVERFLOW = `overflow${EVENT_KEY}` + +const CLASS_NAME_OVERFLOW = 'nav-overflow' +const CLASS_NAME_OVERFLOW_MENU = 'nav-overflow-menu' +const CLASS_NAME_HIDDEN = 'd-none' + +const SELECTOR_NAV_ITEM = '.nav-item' +const SELECTOR_NAV_LINK = '.nav-link' +const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle' +const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu' +const CLASS_NAME_KEEP = 'nav-overflow-keep' + +const Default = { + moreText: 'More', + moreIcon: '', + threshold: 0 // Minimum items to keep visible before showing overflow +} + +const DefaultType = { + moreText: 'string', + moreIcon: 'string', + threshold: 'number' +} + +/** + * Class definition + */ + +class NavOverflow extends BaseComponent { + constructor(element, config) { + super(element, config) + + this._items = [] + this._overflowItems = [] + this._overflowMenu = null + this._overflowToggle = null + this._resizeObserver = null + this._isInitialized = false + + this._init() + } + + // Getters + static get Default() { + return Default + } + + static get DefaultType() { + return DefaultType + } + + static get NAME() { + return NAME + } + + // Public + update() { + this._calculateOverflow() + EventHandler.trigger(this._element, EVENT_UPDATE) + } + + dispose() { + if (this._resizeObserver) { + this._resizeObserver.disconnect() + } + + // Move items back to original positions + this._restoreItems() + + // Remove overflow menu + if (this._overflowToggle && this._overflowToggle.parentElement) { + this._overflowToggle.parentElement.remove() + } + + super.dispose() + } + + // Private + _init() { + // Add overflow class to nav + this._element.classList.add(CLASS_NAME_OVERFLOW) + + // Get all nav items + this._items = [...SelectorEngine.find(SELECTOR_NAV_ITEM, this._element)] + + // Store original order data + for (const [index, item] of this._items.entries()) { + item.dataset.bsNavOrder = index + } + + // Create overflow dropdown if it doesn't exist + this._createOverflowMenu() + + // Setup resize observer + this._setupResizeObserver() + + // Initial calculation + this._calculateOverflow() + + this._isInitialized = true + } + + _createOverflowMenu() { + // Check if overflow menu already exists + this._overflowToggle = SelectorEngine.findOne(SELECTOR_OVERFLOW_TOGGLE, this._element) + + if (this._overflowToggle) { + this._overflowMenu = SelectorEngine.findOne(SELECTOR_OVERFLOW_MENU, this._element) + return + } + + // Create the overflow dropdown item + const overflowItem = document.createElement('li') + overflowItem.className = 'nav-item nav-overflow-item dropdown' + overflowItem.innerHTML = ` + + + ` + + this._element.append(overflowItem) + this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE) + this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU) + + // Initialize dropdown with fixed strategy to escape overflow containers + Dropdown.getOrCreateInstance(this._overflowToggle, { + strategy: 'fixed' + }) + } + + _setupResizeObserver() { + if (typeof ResizeObserver === 'undefined') { + // Fallback for older browsers + EventHandler.on(window, 'resize', () => this._calculateOverflow()) + return + } + + this._resizeObserver = new ResizeObserver(() => { + this._calculateOverflow() + }) + + this._resizeObserver.observe(this._element) + } + + _calculateOverflow() { + // First, restore all items to measure properly + this._restoreItems() + + const navWidth = this._element.offsetWidth + const overflowItem = this._overflowToggle?.closest('.nav-item') + const overflowWidth = overflowItem?.offsetWidth || 0 + + let usedWidth = 0 + const itemsToOverflow = [] + const overflowThreshold = navWidth - overflowWidth - 10 // 10px buffer + + // Calculate which items need to overflow (skip items with keep class) + for (const item of this._items) { + const itemWidth = item.offsetWidth + usedWidth += itemWidth + + // Never overflow items with the keep class + if (item.classList.contains(CLASS_NAME_KEEP)) { + continue + } + + if (usedWidth > overflowThreshold) { + itemsToOverflow.push(item) + } + } + + // Check if we need threshold minimum visible + const visibleCount = this._items.length - itemsToOverflow.length + if (visibleCount < this._config.threshold && this._items.length > this._config.threshold) { + // Add more items to overflow until we reach threshold (but not keep items) + const toMove = this._items.slice(this._config.threshold).filter(item => !item.classList.contains(CLASS_NAME_KEEP)) + itemsToOverflow.length = 0 + itemsToOverflow.push(...toMove) + } + + // Move items to overflow menu + this._moveToOverflow(itemsToOverflow) + + // Show/hide overflow toggle + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN) + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN) + } + } + + // Trigger overflow event if items changed + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }) + } + } + + _moveToOverflow(items) { + if (!this._overflowMenu) { + return + } + + // Clear existing overflow items + this._overflowMenu.innerHTML = '' + this._overflowItems = [] + + for (const item of items) { + // Clone the nav link as a dropdown item + const link = SelectorEngine.findOne(SELECTOR_NAV_LINK, item) + if (!link) { + continue + } + + const dropdownItem = document.createElement('li') + const clonedLink = link.cloneNode(true) + clonedLink.className = 'dropdown-item' + + // Preserve active state + if (link.classList.contains('active')) { + clonedLink.classList.add('active') + } + + // Preserve disabled state + if (link.classList.contains('disabled') || link.hasAttribute('disabled')) { + clonedLink.classList.add('disabled') + } + + dropdownItem.append(clonedLink) + this._overflowMenu.append(dropdownItem) + + // Hide original item + item.classList.add(CLASS_NAME_HIDDEN) + item.dataset.bsNavOverflow = 'true' + + this._overflowItems.push(item) + } + } + + _restoreItems() { + for (const item of this._items) { + item.classList.remove(CLASS_NAME_HIDDEN) + delete item.dataset.bsNavOverflow + } + + if (this._overflowMenu) { + this._overflowMenu.innerHTML = '' + } + + this._overflowItems = [] + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const element of SelectorEngine.find('[data-bs-toggle="nav-overflow"]')) { + NavOverflow.getOrCreateInstance(element) + } +}) + +export default NavOverflow diff --git a/scss/_nav-overflow.scss b/scss/_nav-overflow.scss new file mode 100644 index 0000000000..862c6f280d --- /dev/null +++ b/scss/_nav-overflow.scss @@ -0,0 +1,29 @@ +// Nav Overflow (Priority+ Pattern) +// +// A responsive navigation pattern that automatically moves items +// to an overflow dropdown when space is limited. + +@use "config" as *; +@use "variables" as *; + +@layer components { + .nav-overflow { + flex-wrap: nowrap; + } + + // Container item for overflow + .nav-overflow-item { + flex-shrink: 0; + margin-inline-start: auto; + } + + // Hide items that have been moved to overflow + .nav-overflow [data-bs-nav-overflow="true"] { + display: none; + } + + // Preserve items that should never overflow + .nav-overflow-keep { + flex-shrink: 0; + } +} diff --git a/scss/_nav.scss b/scss/_nav.scss index 773c21dcd4..733c8ce9ad 100644 --- a/scss/_nav.scss +++ b/scss/_nav.scss @@ -10,8 +10,8 @@ $nav-gap: .125rem !default; $nav-link-gap: .5rem !default; $nav-link-align: center !default; $nav-link-justify: center !default; -$nav-link-padding-y: .5rem !default; -$nav-link-padding-x: 1rem !default; +$nav-link-padding-y: .375rem !default; +$nav-link-padding-x: .75rem !default; $nav-link-color: var(--fg-2) !default; $nav-link-hover-color: var(--fg-1) !default; $nav-link-hover-bg: var(--bg-1) !default; @@ -80,6 +80,7 @@ $nav-underline-link-active-color: var(--fg-color) !default; font-weight: var(--nav-link-font-weight); color: var(--nav-link-color); text-decoration: none; + white-space: nowrap; background: none; border: 0; @include border-radius(var(--border-radius)); diff --git a/scss/_navbar.scss b/scss/_navbar.scss index 80f53720d8..2a21fc77ed 100644 --- a/scss/_navbar.scss +++ b/scss/_navbar.scss @@ -11,23 +11,18 @@ @use "mixins/transition" as *; // scss-docs-start navbar-variables -$navbar-padding-y: $spacer * .5 !default; +$navbar-padding-y: $spacer * .25 !default; $navbar-padding-x: null !default; $navbar-nav-link-padding-x: .75rem !default; $navbar-brand-font-size: $font-size-lg !default; -// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link -// mdo-do: fix this -// $nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default; -// $navbar-brand-height: $navbar-brand-font-size * $line-height-base !default; $navbar-brand-height: 1.5rem !default; -// $navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default; $navbar-brand-padding-y: $navbar-brand-height * .5 !default; $navbar-brand-margin-end: 1rem !default; -$navbar-toggler-padding-y: .25rem !default; -$navbar-toggler-padding-x: .75rem !default; +$navbar-toggler-padding-y: .375rem !default; +$navbar-toggler-padding-x: .375rem !default; $navbar-toggler-font-size: $font-size-lg !default; $navbar-toggler-border-radius: var(--border-radius) !default; $navbar-toggler-transition: box-shadow .15s ease-in-out !default; @@ -36,8 +31,6 @@ $navbar-light-color: var(--fg-2) !default; $navbar-light-hover-color: var(--fg-1) !default; $navbar-light-active-color: var(--fg) !default; $navbar-light-disabled-color: var(--fg-3) !default; -$navbar-light-icon-color: color-mix(in oklch, var(--body-color) 75%, transparent) !default; -$navbar-light-toggler-icon-bg: url("data:image/svg+xml,") !default; $navbar-light-toggler-border-color: color-mix(in oklch, var(--fg-body) 15%, transparent) !default; $navbar-light-brand-color: $navbar-light-active-color !default; $navbar-light-brand-hover-color: $navbar-light-active-color !default; @@ -48,14 +41,13 @@ $navbar-dark-color: rgba($white, .55) !default; $navbar-dark-hover-color: rgba($white, .75) !default; $navbar-dark-active-color: $white !default; $navbar-dark-disabled-color: rgba($white, .25) !default; -$navbar-dark-icon-color: $navbar-dark-color !default; -$navbar-dark-toggler-icon-bg: url("data:image/svg+xml,") !default; $navbar-dark-toggler-border-color: rgba($white, .1) !default; $navbar-dark-brand-color: $navbar-dark-active-color !default; $navbar-dark-brand-hover-color: $navbar-dark-active-color !default; // scss-docs-end navbar-dark-variables @layer components { + // Base navbar .navbar { // scss-docs-start navbar-css-vars // stylelint-disable-next-line scss/at-function-named-arguments @@ -74,7 +66,6 @@ $navbar-dark-brand-hover-color: $navbar-dark-active-color !default; --navbar-toggler-padding-y: #{$navbar-toggler-padding-y}; --navbar-toggler-padding-x: #{$navbar-toggler-padding-x}; --navbar-toggler-font-size: #{$navbar-toggler-font-size}; - --navbar-toggler-icon-bg: #{escape-svg($navbar-light-toggler-icon-bg)}; --navbar-toggler-border-color: #{$navbar-light-toggler-border-color}; --navbar-toggler-border-radius: #{$navbar-toggler-border-radius}; --navbar-toggler-transition: #{$navbar-toggler-transition}; @@ -82,15 +73,14 @@ $navbar-dark-brand-hover-color: $navbar-dark-active-color !default; position: relative; display: flex; - flex-wrap: wrap; // allow us to do the line break for collapsing content + flex-wrap: wrap; align-items: center; - justify-content: space-between; // space out brand from logo + justify-content: space-between; padding: var(--navbar-padding-y) var(--navbar-padding-x); + container-type: inline-size; // Enable container queries for responsive behavior @include gradient-bg(); - // Because flex properties aren't inherited, we need to redeclare these first - // few properties so that content nested within behave properly. - // The `flex-wrap` property is inherited to simplify the expanded navbars + // Container properties for nested containers %container-flex-properties { display: flex; flex-wrap: inherit; @@ -133,81 +123,70 @@ $navbar-dark-brand-hover-color: $navbar-dark-active-color !default; // Navbar nav // - // Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`). + // Navigation within navbars. Sets all nav-link CSS variables needed for + // proper styling. Can be used standalone or with `.nav` base class. .navbar-nav { // scss-docs-start navbar-nav-css-vars - // --nav-link-padding-x: 0; - // @mdo-do: fix this, navbar shouldn't need to reuse nav link variables mb? or we need to bring them in… - // --nav-link-padding-y: #{$nav-link-padding-y}; - // @include rfs($nav-link-font-size, --nav-link-font-size); - // --nav-link-font-weight: #{$nav-link-font-weight}; + // Set all nav-link variables for self-contained styling + --nav-gap: .25rem; + --nav-link-gap: .5rem; + --nav-link-padding-x: .5rem; + --nav-link-padding-y: .5rem; --nav-link-color: var(--navbar-color); --nav-link-hover-color: var(--navbar-hover-color); + --nav-link-hover-bg: transparent; + --nav-link-active-color: var(--navbar-active-color); + --nav-link-active-bg: transparent; --nav-link-disabled-color: var(--navbar-disabled-color); // scss-docs-end navbar-nav-css-vars display: flex; - flex-direction: column; // cannot use `inherit` to get the `.navbar`s value + flex-direction: column; + gap: var(--nav-gap); padding-inline-start: 0; margin-bottom: 0; list-style: none; .nav-link { + white-space: nowrap; + &.active, &.show { color: var(--navbar-active-color); } } - - // .dropdown-menu { - // position: static; - // } } // Navbar text // - // + // For adding text or inline elements to the navbar .navbar-text { - // @mdo-do: fix this too - // padding-top: $nav-link-padding-y; - // padding-bottom: $nav-link-padding-y; + padding-top: var(--navbar-brand-padding-y); + padding-bottom: var(--navbar-brand-padding-y); color: var(--navbar-color); a, a:hover, - a:focus { + a:focus { color: var(--navbar-active-color); } } - // Responsive navbar + // Navbar toggler // - // Custom styles for responsive collapsing and toggling of navbar contents. - // Powered by the collapse Bootstrap JavaScript plugin. - - // When collapsed, prevent the toggleable navbar contents from appearing in - // the default flexbox row orientation. Requires the use of `flex-wrap: wrap` - // on the `.navbar` parent. - .navbar-collapse { - flex-grow: 1; - flex-basis: 100%; - // For always expanded or extra full navbars, ensure content aligns itself - // properly vertically. Can be easily overridden with flex utilities. - align-items: center; - } - // Button for toggling the navbar when in its collapsed state + .navbar-toggler { padding: var(--navbar-toggler-padding-y) var(--navbar-toggler-padding-x); font-size: var(--navbar-toggler-font-size); line-height: 1; color: var(--navbar-color); - background-color: transparent; // remove default button style - border: var(--border-width) solid var(--navbar-toggler-border-color); // remove default button style + background-color: transparent; + border: var(--border-width) solid var(--navbar-toggler-border-color); @include border-radius(var(--navbar-toggler-border-radius)); @include transition(var(--navbar-toggler-transition)); @@ -221,101 +200,99 @@ $navbar-dark-brand-hover-color: $navbar-dark-active-color !default; } } - // Keep as a separate element so folks can easily override it with another icon - // or image file as needed. + // Navbar toggler icon (inline SVG) .navbar-toggler-icon { display: inline-block; - width: 1.5em; - height: 1.5em; - vertical-align: middle; - background-image: var(--navbar-toggler-icon-bg); - background-repeat: no-repeat; - background-position: center; - background-size: 100%; + width: 1em; + height: 1em; + color: var(--navbar-color); + vertical-align: -.125em; } - .navbar-nav-scroll { - max-height: var(--scroll-height, 75vh); - overflow-y: auto; - } // scss-docs-start navbar-expand-loop // Generate series of `.navbar-expand-*` responsive classes for configuring - // where your navbar collapses. + // where your navbar collapses and expands. Uses container queries so the + // navbar responds to its own width, not the viewport width. + + // Mixin for expanded state styles (applied to descendants) + @mixin navbar-expanded { + // Style the inner container since we can't style .navbar itself with container queries + > .container, + > .container-fluid, + %navbar-expand-container { + flex-wrap: nowrap; + justify-content: flex-start; + } + + .navbar-nav { + --nav-link-padding-x: var(--navbar-nav-link-padding-x); + flex-direction: row; + } + + .navbar-toggler { + display: none !important; // stylelint-disable-line declaration-no-important + } + + .offcanvas { + // stylelint-disable declaration-no-important + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + @include box-shadow(none); + @include transition(none); + // stylelint-enable declaration-no-important + + .offcanvas-header { + display: none; + } + + .offcanvas-body { + display: flex; + flex-grow: 0; + align-items: center; + padding: 0; + overflow-y: visible; + } + } + } + + // Always expanded (no responsive behavior) .navbar-expand { - @each $breakpoint in map.keys($grid-breakpoints) { - $next: breakpoint-next($breakpoint, $grid-breakpoints); - $infix: breakpoint-infix($next, $grid-breakpoints); - - // stylelint-disable-next-line scss/selector-no-union-class-name - &#{$infix} { - @include media-breakpoint-up($next) { - flex-wrap: nowrap; - justify-content: flex-start; - - .navbar-nav { - --nav-link-padding-x: var(--navbar-nav-link-padding-x); - flex-direction: row; - - // .dropdown-menu { - // position: absolute; - // } - - // .nav-link { - // padding-inline: var(--navbar-nav-link-padding-x); - // } - } - - .navbar-nav-scroll { - overflow: visible; - } - - .navbar-collapse { - display: flex !important; // stylelint-disable-line declaration-no-important - flex-basis: auto; - } - - .navbar-toggler { - display: none !important; // stylelint-disable-line declaration-no-important - } - - .offcanvas { - // stylelint-disable declaration-no-important - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - @include box-shadow(none); - @include transition(none); - // stylelint-enable declaration-no-important - - .offcanvas-header { - display: none; - } - - .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } - } + @include navbar-expanded(); + + // Also set on navbar itself for non-responsive case + flex-wrap: nowrap; + justify-content: flex-start; + } + + // Responsive navbar expand classes using container queries + @each $breakpoint in map.keys($grid-breakpoints) { + $next: breakpoint-next($breakpoint, $grid-breakpoints); + $infix: breakpoint-infix($next, $grid-breakpoints); + $min-width: breakpoint-min($next, $grid-breakpoints); + + @if $next and $min-width { + .navbar-expand#{$infix} { + @container (min-width: #{$min-width}) { + @include navbar-expanded(); } } } } // scss-docs-end navbar-expand-loop + // Navbar themes // - // Styles for switching between navbars with light or dark background. + // Style for dark navbar backgrounds. Use data-bs-theme="dark" for modern approach. - .navbar-dark, .navbar[data-bs-theme="dark"] { // scss-docs-start navbar-dark-css-vars --navbar-color: #{$navbar-dark-color}; @@ -325,15 +302,6 @@ $navbar-dark-brand-hover-color: $navbar-dark-active-color !default; --navbar-brand-color: #{$navbar-dark-brand-color}; --navbar-brand-hover-color: #{$navbar-dark-brand-hover-color}; --navbar-toggler-border-color: #{$navbar-dark-toggler-border-color}; - --navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)}; // scss-docs-end navbar-dark-css-vars } - - @if $enable-dark-mode { - @include color-mode(dark) { - .navbar-toggler-icon { - --navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)}; - } - } - } } diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index 8dfbb55d8c..8d6ad0dcb5 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -6,6 +6,22 @@ @use "mixins/backdrop" as *; @use "layout/breakpoints" as *; +// scss-docs-start offcanvas-variables +$offcanvas-padding-y: $spacer !default; +$offcanvas-padding-x: $spacer !default; +$offcanvas-horizontal-width: 400px !default; +$offcanvas-vertical-height: 30vh !default; +$offcanvas-transition-duration: .3s !default; +$offcanvas-border-color: var(--border-color-translucent) !default; +$offcanvas-border-width: var(--border-width) !default; +$offcanvas-title-line-height: $line-height-base !default; +$offcanvas-bg-color: var(--bg-body) !default; +$offcanvas-color: var(--fg-body) !default; +$offcanvas-box-shadow: var(--box-shadow-lg) !default; +$offcanvas-backdrop-bg: $black !default; +$offcanvas-backdrop-opacity: .5 !default; +// scss-docs-end offcanvas-variables + %offcanvas-css-vars { // scss-docs-start offcanvas-css-vars --offcanvas-zindex: #{$zindex-offcanvas}; @@ -24,6 +40,7 @@ } @layer components { + // Apply CSS vars to all offcanvas responsive variants @each $breakpoint in map.keys($grid-breakpoints) { $next: breakpoint-next($breakpoint, $grid-breakpoints); $infix: breakpoint-infix($next, $grid-breakpoints); @@ -33,6 +50,7 @@ } } + // Responsive offcanvas styles @each $breakpoint in map.keys($grid-breakpoints) { $next: breakpoint-next($breakpoint, $grid-breakpoints); $infix: breakpoint-infix($next, $grid-breakpoints); @@ -53,6 +71,7 @@ @include box-shadow(var(--offcanvas-box-shadow)); @include transition(var(--offcanvas-transition)); + // Placement: Start (left in LTR, right in RTL) &.offcanvas-start { inset-block: 0; inset-inline-start: 0; @@ -65,6 +84,7 @@ } } + // Placement: End (right in LTR, left in RTL) &.offcanvas-end { inset-block: 0; inset-inline-end: 0; @@ -77,6 +97,7 @@ } } + // Placement: Top &.offcanvas-top { inset: 0 0 auto; height: var(--offcanvas-height); @@ -85,6 +106,7 @@ transform: translateY(-100%); } + // Placement: Bottom &.offcanvas-bottom { inset: auto 0 0; height: var(--offcanvas-height); @@ -93,6 +115,18 @@ transform: translateY(100%); } + // Fullscreen variant - covers entire viewport + &.offcanvas-fullscreen { + inset: 0; + width: 100%; + max-width: none; + height: 100%; + max-height: none; + border: 0; + transform: translateY(100%); + } + + // Show/hide states &.showing, &.show:not(.hiding) { transform: none; @@ -105,6 +139,7 @@ } } + // Above breakpoint - show content inline (for responsive offcanvas) @if not ($infix == "") { @include media-breakpoint-up($next) { --offcanvas-height: auto; @@ -120,7 +155,6 @@ flex-grow: 0; padding: 0; overflow-y: visible; - // Reset `background-color` in case `.bg-*` classes are used in offcanvas background-color: transparent !important; // stylelint-disable-line declaration-no-important } } @@ -128,10 +162,12 @@ } } + // Backdrop overlay .offcanvas-backdrop { @include overlay-backdrop($zindex-offcanvas-backdrop, $offcanvas-backdrop-bg, $offcanvas-backdrop-opacity); } + // Header with close button .offcanvas-header { display: flex; align-items: center; @@ -139,7 +175,6 @@ .btn-close { padding: calc(var(--offcanvas-padding-y) * .5) calc(var(--offcanvas-padding-x) * .5); - // Split properties to avoid invalid calc() function if value is 0 margin-inline-start: auto; margin-inline-end: calc(-.5 * var(--offcanvas-padding-x)); margin-top: calc(-.5 * var(--offcanvas-padding-y)); @@ -147,14 +182,28 @@ } } + // Title .offcanvas-title { margin-bottom: 0; line-height: var(--offcanvas-title-line-height); } + // Scrollable body .offcanvas-body { flex-grow: 1; padding: var(--offcanvas-padding-y) var(--offcanvas-padding-x); overflow-y: auto; } + + // Optional footer + .offcanvas-footer { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + gap: .5rem; + align-items: center; + justify-content: flex-end; + padding: var(--offcanvas-padding-y) var(--offcanvas-padding-x); + border-block-start: var(--offcanvas-border-width) solid var(--offcanvas-border-color); + } } diff --git a/scss/bootstrap.scss b/scss/bootstrap.scss index 441b6609b2..89f9112205 100644 --- a/scss/bootstrap.scss +++ b/scss/bootstrap.scss @@ -24,6 +24,7 @@ @forward "dropdown"; @forward "list-group"; @forward "nav"; +@forward "nav-overflow"; @forward "navbar"; @forward "offcanvas"; @forward "pagination"; diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index 1adbb4c977..f25eb46b9b 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -99,8 +99,9 @@ - title: Dialog - title: Dropdown - title: List group - - title: Navbar - title: Navs & tabs + - title: Nav overflow + - title: Navbar - title: Offcanvas - title: Pagination - title: Placeholder diff --git a/site/src/assets/examples/album-rtl/index.astro b/site/src/assets/examples/album-rtl/index.astro index a5e224a00b..1c4662867f 100644 --- a/site/src/assets/examples/album-rtl/index.astro +++ b/site/src/assets/examples/album-rtl/index.astro @@ -32,7 +32,7 @@ export const direction = 'rtl' الألبوم diff --git a/site/src/assets/examples/album/index.astro b/site/src/assets/examples/album/index.astro index 204062c52a..fe40f677fe 100644 --- a/site/src/assets/examples/album/index.astro +++ b/site/src/assets/examples/album/index.astro @@ -31,7 +31,7 @@ import Placeholder from "@shortcodes/Placeholder.astro" Album diff --git a/site/src/assets/examples/carousel-rtl/index.astro b/site/src/assets/examples/carousel-rtl/index.astro index 6a981e4942..6cdbfa973d 100644 --- a/site/src/assets/examples/carousel-rtl/index.astro +++ b/site/src/assets/examples/carousel-rtl/index.astro @@ -10,7 +10,7 @@ import Placeholder from "@shortcodes/Placeholder.astro"
شرائح العرض