From df17f9f5205b9df5fe3023c7d02189dc572da501 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 30 Jun 2026 10:50:10 -0700 Subject: [PATCH] Match Bootstrap 5 default colors and fix docs theme + deep links Style the default selectpicker toggle like a native Bootstrap form-control using --bs-* variables (with fallbacks): match --bs-border-color, keep the input color on hover/active, and show Bootstrap's blue focus ring instead of the legacy webkit outline. Make the Select All/Deselect All actions buttons and the notify/no-results boxes theme-adaptive so they stay legible in dark mode. Docs: bridge Docusaurus' data-theme onto Bootstrap's data-bs-theme so the live examples render correctly in dark mode, and add a deep-link scroll fix that re-pins the viewport to the hash target while the async live demos reflow the page. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/docusaurus.config.js | 5 ++ docs/src/bootstrapTheme.js | 24 ++++++++++ docs/src/deepLinkScroll.js | 97 ++++++++++++++++++++++++++++++++++++++ sass/bootstrap-select.scss | 57 ++++++++++++++++++---- 4 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 docs/src/bootstrapTheme.js create mode 100644 docs/src/deepLinkScroll.js diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index c698958b..85dd334d 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -25,6 +25,11 @@ const config = { } }, + clientModules: [ + require.resolve('./src/bootstrapTheme.js'), + require.resolve('./src/deepLinkScroll.js') + ], + scripts: [ { src: withBaseUrl('js/chunk-recovery.js') diff --git a/docs/src/bootstrapTheme.js b/docs/src/bootstrapTheme.js new file mode 100644 index 00000000..2a6cc5b6 --- /dev/null +++ b/docs/src/bootstrapTheme.js @@ -0,0 +1,24 @@ +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +// Docusaurus toggles its color mode by setting `data-theme="light|dark"` on +// the element, but Bootstrap (and therefore the bootstrap-select +// dropdowns rendered in the docs) reads `data-bs-theme`. Without this bridge +// the Bootstrap components stay light while the rest of the page is dark. +// This mirrors Docusaurus' theme onto Bootstrap so the live examples reflect +// both light and dark modes accurately. +if (ExecutionEnvironment.canUseDOM) { + const root = document.documentElement; + + const syncBootstrapTheme = function () { + const theme = root.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; + + if (root.getAttribute('data-bs-theme') !== theme) { + root.setAttribute('data-bs-theme', theme); + } + }; + + syncBootstrapTheme(); + + const observer = new MutationObserver(syncBootstrapTheme); + observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] }); +} diff --git a/docs/src/deepLinkScroll.js b/docs/src/deepLinkScroll.js new file mode 100644 index 00000000..f51efbfc --- /dev/null +++ b/docs/src/deepLinkScroll.js @@ -0,0 +1,97 @@ +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +// The examples/options pages render live bootstrap-select demos that initialize +// asynchronously and grow the page height AFTER Docusaurus has already done its +// initial hash scroll. The result is that deep links (e.g. .../examples/#some-id) +// land at the wrong offset - usually the top of the page - which makes the target +// section look like it does not exist. +// +// Docusaurus also enables smooth scrolling and performs its own scroll handling on +// hydration, so a one-shot scroll is unreliable. Instead, once the route content is +// mounted we continuously re-pin the viewport to the hash target (instantly, +// bypassing smooth scrolling) for a short window so it stays aligned while the demos +// reflow the page. We stop the moment the visitor interacts so we never hijack their +// scrolling. `onRouteDidUpdate` is the key hook: it fires AFTER the page content has +// rendered, which is when the target heading actually exists in the DOM. +const INTERACTION_EVENTS = ['wheel', 'touchstart', 'keydown', 'mousedown']; +const PIN_DURATION_MS = 3000; + +let activePin = null; + +function getHashTarget () { + const hash = window.location.hash; + + if (!hash || hash.length < 2) return null; + + let id; + + try { + id = decodeURIComponent(hash.slice(1)); + } catch (error) { + id = hash.slice(1); + } + + return document.getElementById(id); +} + +function startPin () { + if (!ExecutionEnvironment.canUseDOM) return; + + const hash = window.location.hash; + + if (!hash || hash.length < 2) return; + + if (activePin) activePin.stop(); + + let cancelled = false; + let rafId; + const start = Date.now(); + + function cleanup () { + if (rafId) window.cancelAnimationFrame(rafId); + + INTERACTION_EVENTS.forEach(function (type) { + window.removeEventListener(type, stop); + }); + + if (activePin && activePin.stop === stop) activePin = null; + } + + function stop () { + cancelled = true; + cleanup(); + } + + INTERACTION_EVENTS.forEach(function (type) { + window.addEventListener(type, stop, { passive: true }); + }); + + activePin = { stop: stop }; + + function tick () { + if (cancelled) return; + + const target = getHashTarget(); + + if (target) { + target.scrollIntoView({ block: 'start', behavior: 'auto' }); + } + + if (Date.now() - start < PIN_DURATION_MS) { + rafId = window.requestAnimationFrame(tick); + } else { + cleanup(); + } + } + + tick(); +} + +export function onRouteDidUpdate () { + startPin(); +} + +if (ExecutionEnvironment.canUseDOM) { + window.addEventListener('load', startPin); + window.addEventListener('hashchange', startPin); +} diff --git a/sass/bootstrap-select.scss b/sass/bootstrap-select.scss index 748f6e57..bc59cb23 100644 --- a/sass/bootstrap-select.scss +++ b/sass/bootstrap-select.scss @@ -111,11 +111,35 @@ select.selectpicker { } > select.mobile-device:focus + .dropdown-toggle, - .dropdown-toggle:focus { - outline: thin dotted #333333 !important; - outline: 5px auto -webkit-focus-ring-color !important; - outline-offset: -2px; - } + .dropdown-toggle:focus, + .dropdown-toggle:focus-visible { + outline: 0 !important; + border-color: var(--bs-primary, #86b7fe); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb, 13, 110, 253), 0.25); + } +} + +// Make the default toggle button look and behave like a native Bootstrap +// form-control. Its colors are driven entirely by Bootstrap's CSS variables +// (with fallbacks) so the control stays visually identical to inputs in both +// light and dark modes, follows the site's --bs-border-color, and never +// darkens on hover/active the way the stock .btn-light does. Consumers can +// restyle it globally by overriding the Bootstrap variables, and choosing a +// different `style` (e.g. btn-primary) opts out of these defaults entirely. +.bootstrap-select > .dropdown-toggle.btn-light { + --bs-btn-color: var(--bs-body-color, #212529); + --bs-btn-bg: var(--bs-body-bg, #fff); + --bs-btn-border-color: var(--bs-border-color, #dee2e6); + --bs-btn-hover-color: var(--bs-body-color, #212529); + --bs-btn-hover-bg: var(--bs-body-bg, #fff); + --bs-btn-hover-border-color: var(--bs-border-color, #dee2e6); + --bs-btn-active-color: var(--bs-body-color, #212529); + --bs-btn-active-bg: var(--bs-body-bg, #fff); + --bs-btn-active-border-color: var(--bs-border-color, #dee2e6); + --bs-btn-disabled-color: var(--bs-secondary-color, #6c757d); + --bs-btn-disabled-bg: var(--bs-secondary-bg, #e9ecef); + --bs-btn-disabled-border-color: var(--bs-border-color, #dee2e6); + --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb, 13, 110, 253); } // The selectpicker components @@ -337,8 +361,9 @@ select.selectpicker { margin: 0 2%; min-height: 26px; padding: 3px 5px; - background: rgb(245, 245, 245); - border: 1px solid rgb(227, 227, 227); + background: var(--bs-tertiary-bg, rgb(245, 245, 245)); + color: var(--bs-secondary-color, inherit); + border: 1px solid var(--bs-border-color, rgb(227, 227, 227)); box-shadow: inset 0 1px 1px fade(rgb(0, 0, 0), 5%); pointer-events: none; opacity: 0.9; @@ -352,7 +377,8 @@ select.selectpicker { .no-results { padding: 3px; - background: #f5f5f5; + background: var(--bs-tertiary-bg, #f5f5f5); + color: var(--bs-secondary-color, inherit); margin: 0 5px; white-space: nowrap; } @@ -592,6 +618,21 @@ select.selectpicker { width: 50%; } } + + // Drive the Select All / Deselect All buttons from Bootstrap variables so + // they stay legible in dark mode instead of remaining light-on-light. + & .btn-light { + --bs-btn-color: var(--bs-body-color, #212529); + --bs-btn-bg: var(--bs-tertiary-bg, #f8f9fa); + --bs-btn-border-color: var(--bs-border-color, #dee2e6); + --bs-btn-hover-color: var(--bs-body-color, #212529); + --bs-btn-hover-bg: var(--bs-secondary-bg, #e9ecef); + --bs-btn-hover-border-color: var(--bs-border-color, #dee2e6); + --bs-btn-active-color: var(--bs-body-color, #212529); + --bs-btn-active-bg: var(--bs-secondary-bg, #e9ecef); + --bs-btn-active-border-color: var(--bs-border-color, #dee2e6); + --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb, 13, 110, 253); + } } .bs-donebutton {