Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const config = {
}
},

clientModules: [
require.resolve('./src/bootstrapTheme.js'),
require.resolve('./src/deepLinkScroll.js')
],

scripts: [
{
src: withBaseUrl('js/chunk-recovery.js')
Expand Down
24 changes: 24 additions & 0 deletions docs/src/bootstrapTheme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

// Docusaurus toggles its color mode by setting `data-theme="light|dark"` on
// the <html> 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'] });
}
97 changes: 97 additions & 0 deletions docs/src/deepLinkScroll.js
Original file line number Diff line number Diff line change
@@ -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);
}
57 changes: 49 additions & 8 deletions sass/bootstrap-select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down