Skip to content

Add Bootstrap 5.3 color mode (data-bs-theme) support#435

Open
dereuromark wants to merge 8 commits into
masterfrom
feature-color-mode
Open

Add Bootstrap 5.3 color mode (data-bs-theme) support#435
dereuromark wants to merge 8 commits into
masterfrom
feature-color-mode

Conversation

@dereuromark
Copy link
Copy Markdown
Member

@dereuromark dereuromark commented May 11, 2026

Summary

Adds Bootstrap 5.3 color mode (data-bs-theme) support — the flagship BS5.3 feature that the plugin currently has zero coverage for (a grep -rn 'data-bs-theme\|color-mode' src/ templates/ webroot/ against master returns nothing).

The new ColorModeHelper provides the two pieces an app needs to actually use color modes without writing custom JS:

$this->ColorMode->script() — applies the stored theme on page load

Emits a small inline IIFE that:

  • reads the user's choice from localStorage (default key: bs-theme),
  • resolves auto against window.matchMedia('(prefers-color-scheme: dark)'),
  • sets data-bs-theme on the configured target (default: <html>),
  • re-resolves when the OS preference changes while in auto mode,
  • exposes window.BootstrapUIColorMode.{get, set}(mode) for app code,
  • dispatches a bs-theme-changed DOM event whenever the resolved theme changes.

Placed near the top of <head>, it prevents the flash of unstyled theme that every other "add dark mode in the layout" approach has.

$this->ColorMode->toggle() — ready-made switcher widget

Renders a Bootstrap btn-group with one button per configured mode (default: light / dark / auto). Clicking a button calls BootstrapUIColorMode.set(), toggles aria-pressed and the active class, and fires the event. ARIA-labelled, escapes user-supplied labels.

Usage

// Layout <head>, as early as possible:
<?= $this->ColorMode->script() ?>

// Navbar or wherever the switcher belongs:
<?= $this->ColorMode->toggle() ?>

Config knobs (per-call or via loadHelper): storageKey, default, target, modes, labels, ariaLabel, wrapperClass, buttonClass, activeClass. Labels go through __() if you pass them through; the helper itself escapes them.

Wiring

  • New class: src/View/Helper/ColorModeHelper.php
  • Auto-loaded by UIViewTrait as $this->ColorMode.
  • @property added to UIView docblock.
  • README has a new section under Usage explaining usage, public JS API, and config keys.

Notes

  • This is a feature, not a fix. Independent of Fix five real defects in helpers and console commands #434 (bug-fix PR) — either can land first.
  • No new asset files; the JS is inlined so apps don't have to ship a separate file or symlink anything new.
  • No changes to existing helpers — purely additive.
  • Existing layouts/examples still ship neutral CSS classes like bg-light/text-dark in places; those are not touched here. A follow-up could audit templates/layout/examples/* and FlashHelper.php for hardcoded neutrals if you want full dark-mode parity in the bundled layouts.

A new ColorModeHelper provides the two pieces that have been
missing for color-mode adoption since the BS5.3 migration:

- `script()` emits an inline IIFE that reads the user's stored
  preference from localStorage (falling back to the OS preference
  for `auto`), applies `data-bs-theme` to the configured target
  (default `<html>`), re-resolves on `prefers-color-scheme`
  changes while in auto mode, and exposes a tiny public API
  (`window.BootstrapUIColorMode.{get,set}` plus a
  `bs-theme-changed` DOM event). Placed in `<head>` it prevents
  the flash of unstyled theme.
- `toggle()` renders a Bootstrap button group with one button per
  configured mode. Clicks call `BootstrapUIColorMode.set()`,
  update the active state, and announce the change via the event.

Configurable: storageKey, default, target, modes, labels (i18n
hook via `__()`), ariaLabel, wrapperClass, buttonClass,
activeClass.

The helper is auto-loaded by `UIViewTrait` as `$this->ColorMode`.
README has a new "Color modes" section under Usage.

Tests cover default and custom-config rendering for both methods
and verify user-supplied labels are HTML-escaped.
Comment thread src/View/Helper/ColorModeHelper.php Outdated
Per review: extract the three string constants (MODE_LIGHT,
MODE_DARK, MODE_AUTO) into a BootstrapUI\View\Helper\ColorMode
enum. The helper's `default` and `modes` config keys now accept
either enum cases or plain strings interchangeably, so existing
apps that pass `'light'` / `'dark'` / `'auto'` keep working while
new code can use the typed API.

A small `_modeValue()` helper coerces a value to its string form
for places that render or serialize.

Updates the README example to use the enum and notes that strings
are still accepted. Adds tests covering enum usage on both
`modes` and `default`.
@dereuromark
Copy link
Copy Markdown
Member Author

Addressed in 2112231. Extracted the three string constants into a BootstrapUI\View\Helper\ColorMode backed-string enum. The helper's default and modes config keys accept either enum cases or plain strings interchangeably so existing apps that pass 'light'/'dark'/'auto' keep working, while new code can use the typed API:

use BootstrapUI\View\Helper\ColorMode;

echo $this->ColorMode->toggle([
    'modes' => [ColorMode::Light, ColorMode::Dark],
    'default' => ColorMode::Dark,
]);

A small _modeValue() helper coerces either form to its string value for rendering. README updated with the enum import. Two new tests cover enum usage on both modes and default.

Comment thread src/View/Helper/ColorModeHelper.php Outdated
ColorMode now implements Cake's EnumLabelInterface and provides its own
translated label() — matches FormHelper's enum-label convention and removes
the now-redundant 'labels' config on the helper. Custom string modes still
fall back to ucfirst().
@dereuromark dereuromark requested a review from ADmad May 13, 2026 13:08
Comment thread src/View/Helper/ColorModeHelper.php Outdated
The previous default literally duplicated ColorMode::cases(). Use null
as the sentinel for "all cases in declaration order" and keep the
config key for apps that want to reorder, subset, or add custom theme
strings.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Bootstrap 5.3 color mode (data-bs-theme) support to BootstrapUI by introducing a new ColorModeHelper (inline bootstrapper script + toggle UI), wiring it into UIViewTrait auto-loading, and documenting/covering it with tests.

Changes:

  • Added ColorModeHelper to emit a page-load script that applies the persisted/resolved theme and to render a button-group toggle.
  • Added a ColorMode enum (implements CakePHP’s EnumLabelInterface) to provide translated labels and typed mode values.
  • Auto-loaded the helper via UIViewTrait, updated UIView docblock, added README docs, and added PHPUnit coverage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/View/Helper/ColorModeHelper.php New helper implementing the inline theme-applier script and the toggle widget.
src/View/Helper/Enum/ColorMode.php New enum for light/dark/auto with translatable labels.
src/View/UIViewTrait.php Auto-loads the new helper as ColorMode.
src/View/UIView.php Adds @property for the new helper.
tests/TestCase/View/UIViewTest.php Verifies the helper is auto-loaded on UIView.
tests/TestCase/View/Helper/ColorModeHelperTest.php Covers default/custom rendering of script() and toggle() and enum label behavior.
README.md Documents usage, JS API, event, and configuration keys for color modes.
Comments suppressed due to low confidence (1)

src/View/Helper/ColorModeHelper.php:128

  • The inline JS in toggle() also embeds config-derived strings via json_encode() (default mode / activeClass) inside a <script> tag. For the same reason as in script(), this should use safe encoding flags (eg JSON_HEX_TAG | ...) to avoid </script>-based breakouts if these values contain </>.
            . 'var active=(window.BootstrapUIColorMode&&BootstrapUIColorMode.get())||'
            . json_encode($this->_modeValue($config['default']))
            . ';'
            . 'var ac=' . json_encode($config['activeClass']) . ';'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/View/Helper/ColorModeHelper.php Outdated
Comment thread src/View/Helper/ColorModeHelper.php Outdated
Comment thread src/View/Helper/ColorModeHelper.php Outdated
Comment thread src/View/Helper/ColorModeHelper.php
…alStorage guards.

- Toggle click handler now syncs aria-pressed alongside the active class
  via a shared sync() helper, so assistive tech sees the same state as
  visual users.
- Config values inlined into <script> are encoded with JSON_HEX_TAG +
  HEX_AMP/APOS/QUOT (centralized in _jsonValue()) so a stray </script>
  in storageKey/target/activeClass cannot terminate the script tag.
- localStorage.getItem/setItem calls wrapped in try/catch — when storage
  is blocked (private mode, security policy) the script still applies
  the theme rather than throwing and leaving an unstyled flash.
- _modeValue() docblock no longer overpromises lower-casing; raw strings
  keep their casing so custom-theme registrations are preserved.
@dereuromark dereuromark requested a review from ADmad May 13, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants