Skip to content

Add "Customize cookie settings" button and preferences panel - #13

Open
Castellon-ACM wants to merge 4 commits into
fix/reopen-trigger-and-link-a11yfrom
feature/customize-preferences-panel
Open

Castellon-ACM wants to merge 4 commits into
fix/reopen-trigger-and-link-a11yfrom
feature/customize-preferences-panel

Conversation

@Castellon-ACM

Copy link
Copy Markdown
Contributor

Summary

Closes #11.

Implements the "Customize cookie settings" button and cookie preferences panel described in the issue, built on top of PR #12 (fix/reopen-trigger-and-link-a11y, not yet merged) since both touch the exact same banner markup/JS (the icon-only reopen trigger and policy-page-title link PR #12 just added). This PR's base branch is fix/reopen-trigger-and-link-a11y, not main — once #12 merges, this should be re-based/re-targeted at main.

Acceptance criteria (from the issue)

  • The button appears on the left of the action row with the cookie icon, in bar, box and popup layouts, on desktop and mobile. Printed through the existing frcn_cookie_notice_before_actions action; reuses the same CSS-mask icon technique as .frcn-cookie-notice__icon.
  • Clicking it opens the preferences panel; Esc / close button dismiss it and restore focus. Also reachable from the persistent reopen trigger — both triggers open the same #frcn-cookie-preferences dialog and restore focus to whichever one opened it.
  • Accept all, Reject all and Save changes produce the same cookie, Consent Mode update and logging as the current buttons — all three (and the banner's own Accept/Reject) call the same handleDecision() in JS and the same log_consent_callback() AJAX endpoint server-side. No parallel consent logic.
  • The panel works with a full-page cache: printed unconditionally (including on the policy page), hidden via the hidden attribute, identical HTML for every visitor — open/closed state is entirely client-side.
  • Without JavaScript, the banner still works as it does today — the Customize button is hidden via the existing <noscript> fallback (print_noscript_style()); Accept/Reject keep working via the no-JS <form> fallback.
  • Hooks/filters exist and are documented in docs/preferences-panel-hooks.md:
    • frcn_cookie_notice_before_actions (existing, reused for the button)
    • frcn_cookie_preferences_categories (new action, fires inside the panel)
    • frcn_cookie_consent_categories (new filter, applied around the per-category payload in log_consent_callback())
    • frcn_cookie_customize_button_label / frcn_cookie_customize_button_enabled (new filters)
  • With PRO inactive (nothing hooked to frcn_cookie_preferences_categories), no category UI is rendered — verified in test_no_category_ui_is_rendered_without_an_add_on. With a test add-on hooked in (see test_preferences_categories_action_fires_in_the_right_place and CookieNoticeConsentCategoriesTest), categories show up and are read back through the filter.
  • All strings are translatable (text domain frontconsent) and in English; button/panel are keyboard and screen-reader friendly (dialog ARIA, focus trap, live-region announcements reusing the existing announcer).
  • Tests added/updated; composer lint and composer phpstan pass; readme.txt/readme.md updated.

What was built

  • PHP (includes/Frontend/CookieNotice.php): render_customize_button() (hooked on frcn_cookie_notice_before_actions), render_preferences_panel() (called from render_banner() next to the reopen trigger, unconditionally), the new action/filters above, and get_consent_categories_payload() wiring the categories filter into log_consent_callback()'s JSON response.
  • CSS: secondary/outline styling for the button (reusing the existing accent/bg/radius CSS custom properties) and a new dialog styles for #frcn-cookie-preferences.
  • JS: hoisted the decision-recording code path (handleDecision, setConsentCookie, updateConsentMode, logDecision, dispatchConsentEvent) out from under the "only if banner exists" gate so it also works on the policy page; added the preferences panel's own focus trap/Escape handling (guarding the banner's own popup trap so only one dialog reacts to Escape/Tab at a time); changed the reopen trigger to open this panel instead of clearing the cookie and reloading.
  • Docs: docs/preferences-panel-hooks.md documents every hook/filter, when it fires, and what data is available, plus a minimal PRO-style example add-on.
  • Tests: tests/Unit/CookieNoticePreferencesPanelTest.php (button/panel markup, ARIA, the categories action firing, the label/enabled filters, no-JS hiding), tests/Unit/CookieNoticeConsentCategoriesTest.php (the categories filter applied around the AJAX payload), plus updates to CookieNoticeAccessibilityTest.php (scoped an existing assertion now that the panel's own dialog markup is always present) and both tests/js/*.test.js files covering the new panel's open/close/focus-trap/decision-routing behavior.

Test results

  • composer test (PHPUnit): 96 tests, 187 assertions, all passing — ran locally against a throwaway MySQL 8.0 instance (Local by Flywheel's bundled binaries, scratch datadir, non-default port), torn down afterward.
  • composer lint (phpcs): clean.
  • composer phpstan: clean, no errors.
  • npm run test:js (node --test): 39 tests, all passing.
  • CI will additionally verify PHP 7.4/8.1/8.2/8.3 matrices this session couldn't run locally (only PHP 8.3 was available).

Not done / out of scope

🤖 Generated with Claude Code

Castellon-ACM and others added 4 commits September 30, 2026 13:12
Adds a real <button> to the left of Reject/Accept, showing the existing
cookie icon, printed through the existing frcn_cookie_notice_before_actions
action so a PRO add-on can hook the same extension point. Clicking it (or
the persistent reopen trigger) opens a modal preferences panel
(role="dialog", aria-modal="true") with a static "Strictly necessary"
section and Accept all / Reject all / Save changes actions, all funneled
through the exact same client-side decision-recording function the
banner's own Accept/Reject buttons already use, and the same AJAX
log_consent_callback() server-side.

PRO-ready extension points, per the issue:
- New frcn_cookie_preferences_categories action, fired inside the panel
  where a PRO add-on renders its own per-category toggles.
- New frcn_cookie_consent_categories filter, applied around the optional
  per-category map in log_consent_callback()'s response — purely additive,
  the binary accepted/rejected cookie format is unchanged.
- New frcn_cookie_customize_button_label / frcn_cookie_customize_button_enabled
  filters for the button's copy and visibility.

The panel is printed unconditionally (including on the policy page) and
hidden by default via the `hidden` attribute, keeping the output
cache-neutral like the rest of the banner. Without JS the button is hidden
via the existing <noscript> fallback; Accept/Reject keep working through
the no-JS <form> fallback as before.

Also updates the reopen trigger to open this same panel instead of
clearing the consent cookie and reloading the page — now that the panel
renders even on the policy page, there's no need to navigate away to
reach it.

Documented in docs/preferences-panel-hooks.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rketing/frontconsent into feature/customize-preferences-panel

# Conflicts:
#	assets/cookie-notice/frontconsent-cookie-notice.css
#	assets/cookie-notice/frontconsent-cookie-notice.min.css
…ions

The panel had no visible feedback: Accept all/Reject all/Save changes
all recorded a decision, but the panel's own markup looked identical
regardless of what was clicked, since the only category shown was the
static, always-on "Strictly necessary" block. A visitor had no way to
tell whether their action had any effect.

Add a real "Analytics & Marketing" checkbox (data-frcn-category="optional")
after the necessary block. frontconsent-cookie-notice.js already had the
plumbing to read panel toggles on Save changes/Accept all
(collectPreferencesCategories()) — it only lacked an actual toggle to
read. Added the missing half: syncPreferencesToggles(), called every time
the panel opens, reflects the checkbox from the real consent cookie
(accepted/rejected), which stays the single source of truth — the
toggle is presentation, not a second stored state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Analytics and Marketing gated the exact same binary accepted/rejected
decision despite showing as separate concepts ("analytics y marketing
son diferentes"). Replace the single "Analytics & Marketing" checkbox
with two independent ones (data-frcn-category="analytics"/"marketing")
and make the split functionally meaningful end to end:

- frontconsent-cookie-notice.js persists the visitor's actual
  per-category selection into a new frontconsent_categories cookie
  (JSON, e.g. {"analytics":true,"marketing":false}) alongside the
  existing binary consent cookie, and syncs each toggle's checked
  state from it independently when the panel opens.
- CookieNotice::get_config_callback() reads that cookie server-side
  and builds a real allowedCategories map (still filterable via
  frcn_cookie_notice_allowed_tracking_categories for PRO), instead of
  always returning null. frcnCookieNoticeInject() already gated GTM/
  GA4 ('analytics') separately from Clientify/Brevo/OpenAI ads
  ('marketing') on this map — it just needed a real value to gate on.
- Backward compatible: a visitor who accepted before this cookie
  existed has no frontconsent_categories cookie yet; both categories
  default to allowed for them (matching the previous allow-all
  behavior) until they make an explicit per-category choice.

Updates existing PHPUnit/JS tests for the new markup and cookie logic,
and adds coverage for the malformed-cookie fallback and the backward-
compat default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant