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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ Prefix the change with one of these keywords:

## [Unreleased]

- _Added_: `ColorToolsPanel` component replicating core's private color panel wrapper (`color-block-support-panel` class, `hasInnerWrapper`, first/last item classes) for identical grouped-item styling
- _Changed_: Migrated `useSetting` to `useSettings` (stable block editor API)
- _Changed_: Border controls updated to use stable `BorderBoxControl` and `BorderRadiusControl` imports, replacing experimental aliases
- _Changed_: Replaced `PanelColorSettings` with `ColorGradientSettingsDropdown` and `useMultipleOriginColorsAndGradients` in the button color panel, menu color panel, and submenu color panel — matches core's own color control pattern and restores the CSS grid layout shared by all other panels
- _Changed_: Replaced regex-based HTML attribute injection in `Block_Renderer` with `WP_HTML_Tag_Processor`, aligning with core's own render-block pattern
- _Changed_: Frontend assets now register early on `wp_enqueue_scripts` and enqueue at render time, so scripts and styles only load on pages that contain a Priority+ navigation block
- _Changed_: Updated FAQ entry for the "Always" overlay option to reflect the new normalize-to-mobile behaviour
- _Removed_: `addDisableAlwaysOption` HOC that manipulated the DOM to disable the "Always" overlay button; selecting "Always" now normalizes back to "Mobile" via an effect, with an explanatory inspector notice


## [1.1.0]

- _Added_: Editor preview of the More button that reflects label, colors, and padding settings
Expand Down
2 changes: 1 addition & 1 deletion build/priority-plus-nav-editor.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => 'b305fc15ec1f3200a37a');
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => '326c37c6d0eef4efc876');
2 changes: 1 addition & 1 deletion build/priority-plus-nav-editor.js

Large diffs are not rendered by default.

70 changes: 36 additions & 34 deletions classes/class-block-renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ private function collect_attributes( array $block ): array {
/**
* Inject Priority+ data attributes into the navigation element.
*
* Uses WP_HTML_Tag_Processor to modify the opening nav tag, mirroring how
* core injects directives into rendered navigation markup.
*
* @param string $block_content The block HTML content.
* @param array $attributes Collected attributes array.
* @return string Modified block content with data attributes.
Expand All @@ -195,45 +198,47 @@ private function inject_priority_attributes( string $block_content, array $attri
return $block_content;
}

// Match the opening <nav> tag with wp-block-navigation class.
$pattern = '/(<nav[^>]*\bclass="[^"]*wp-block-navigation[^"]*")/i';
$processor = new \WP_HTML_Tag_Processor( $block_content );

// Build style parts array.
$style_parts = $this->build_style_parts( $block_content, $attributes );
if ( ! $processor->next_tag(
array(
'tag_name' => 'NAV',
'class_name' => 'wp-block-navigation',
)
) ) {
return $block_content;
}

// Build data attributes string.
$data_attributes = sprintf(
'$1 data-more-label="%s" data-more-icon="%s" data-overlay-menu="%s" data-mobile-collapse="%s"',
esc_attr( $attributes['toggle_label'] ),
esc_attr( $attributes['toggle_icon'] ),
esc_attr( $attributes['overlay_menu'] ),
$attributes['mobile_collapse'] ? 'true' : 'false'
$existing_style = $processor->get_attribute( 'style' );
$style_parts = $this->build_style_parts(
is_string( $existing_style ) ? $existing_style : '',
$attributes
);

// Add style attribute if we have any styles.
$processor->set_attribute( 'data-more-label', $attributes['toggle_label'] );
$processor->set_attribute( 'data-more-icon', $attributes['toggle_icon'] );
$processor->set_attribute( 'data-overlay-menu', $attributes['overlay_menu'] );
$processor->set_attribute( 'data-mobile-collapse', $attributes['mobile_collapse'] ? 'true' : 'false' );

if ( ! empty( $style_parts ) ) {
$style_attr = implode( '; ', $style_parts );
$data_attributes .= ' style="' . $style_attr . ';"';
$processor->set_attribute( 'style', implode( '; ', $style_parts ) . ';' );
}

// Remove existing style attribute from the nav tag if it exists.
$block_content = preg_replace( '/(<nav[^>]*?)\s+style="[^"]*"([^>]*?>)/i', '$1$2', $block_content, 1 );

return preg_replace( $pattern, $data_attributes, $block_content, 1 );
return $processor->get_updated_html();
}

/**
* Build array of CSS style declarations.
*
* @param string $block_content Original block content (to preserve existing styles).
* @param array $attributes Collected attributes.
* @param string $existing_style Existing inline style attribute value on the nav element.
* @param array $attributes Collected attributes.
* @return array Array of CSS style declarations.
*/
private function build_style_parts( string $block_content, array $attributes ): array {
private function build_style_parts( string $existing_style, array $attributes ): array {
$style_parts = array();

// First, preserve WordPress's existing inline styles (typography, etc.).
$style_parts = $this->preserve_existing_styles( $block_content, $style_parts );
$style_parts = $this->preserve_existing_styles( $existing_style, $style_parts );

// Add toggle button styles.
$style_parts = $this->add_toggle_styles( $attributes, $style_parts );
Expand All @@ -245,21 +250,18 @@ private function build_style_parts( string $block_content, array $attributes ):
}

/**
* Preserve existing inline styles from the block content.
* Preserve existing inline style declarations.
*
* @param string $block_content Block HTML content.
* @param array $style_parts Current style parts array.
* @param string $existing_style Existing inline style attribute value.
* @param array $style_parts Current style parts array.
* @return array Updated style parts array.
*/
private function preserve_existing_styles( string $block_content, array $style_parts ): array {
if ( preg_match( '/<nav[^>]*\bstyle="([^"]*)"/i', $block_content, $style_matches ) ) {
$existing_style = $style_matches[1];
$style_declarations = explode( ';', $existing_style );
foreach ( $style_declarations as $declaration ) {
$declaration = trim( $declaration );
if ( ! empty( $declaration ) ) {
$style_parts[] = $declaration;
}
private function preserve_existing_styles( string $existing_style, array $style_parts ): array {
$style_declarations = explode( ';', $existing_style );
foreach ( $style_declarations as $declaration ) {
$declaration = trim( $declaration );
if ( ! empty( $declaration ) ) {
$style_parts[] = $declaration;
}
}
return $style_parts;
Expand Down
67 changes: 50 additions & 17 deletions classes/class-enqueues.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,45 @@ public function __construct( string $build_path ) {
*/
public function init() {
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'register_frontend_assets' ) );
}

/**
* Register (without enqueueing) the frontend script and style.
*
* Registration happens on wp_enqueue_scripts so the handles exist early;
* the actual enqueue is deferred to render time (via Block_Renderer) so
* assets only load on pages that render a Priority+ navigation.
*
* @return void
*/
public function register_frontend_assets(): void {
if ( wp_script_is( 'priority-plus-navigation', 'registered' ) ) {
return;
}

$asset_meta = $this->build_dir->get_asset_meta( 'priority-plus-navigation.js' );
if ( ! $asset_meta ) {
return;
}

wp_register_script(
'priority-plus-navigation',
$this->build_dir->get_url( 'priority-plus-navigation.js' ),
$asset_meta['dependencies'],
$asset_meta['version'],
true
);

$style_path = $this->build_dir->get_path( 'style-priority-plus-navigation.css' );
if ( file_exists( $style_path ) ) {
wp_register_style(
'priority-plus-navigation',
$this->build_dir->get_url( 'style-priority-plus-navigation.css' ),
array(),
$asset_meta['version']
);
}
}

/**
Expand Down Expand Up @@ -88,36 +127,30 @@ public function enqueue_editor_assets(): void {
* Enqueue Priority+ frontend script and styles (only once).
* Called from Block_Renderer when a Priority+ block is rendered.
*
* Only the pre-registered handles are enqueued here; registering happens
* on wp_enqueue_scripts (with a fallback registration for render contexts
* where that hook never fired, e.g. REST block rendering).
*
* @return void
*/
public function enqueue_frontend_assets(): void {
if ( $this->frontend_assets_enqueued ) {
return;
}

$asset_meta = $this->build_dir->get_asset_meta( 'priority-plus-navigation.js' );
if ( ! $asset_meta ) {
// Fallback for contexts where wp_enqueue_scripts did not run.
$this->register_frontend_assets();

if ( ! wp_script_is( 'priority-plus-navigation', 'registered' ) ) {
return;
}

$this->frontend_assets_enqueued = true;

wp_enqueue_script(
'priority-plus-navigation',
$this->build_dir->get_url( 'priority-plus-navigation.js' ),
$asset_meta['dependencies'],
$asset_meta['version'],
true
);
wp_enqueue_script( 'priority-plus-navigation' );

$style_path = $this->build_dir->get_path( 'style-priority-plus-navigation.css' );
if ( file_exists( $style_path ) ) {
wp_enqueue_style(
'priority-plus-navigation',
$this->build_dir->get_url( 'style-priority-plus-navigation.css' ),
array(),
$asset_meta['version']
);
if ( wp_style_is( 'priority-plus-navigation', 'registered' ) ) {
wp_enqueue_style( 'priority-plus-navigation' );
}
}
}
45 changes: 45 additions & 0 deletions planned-updates/01-platform-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 01 — Platform Baseline: Requirements, Metadata, and Tooling

| | |
|---|---|
| **Priority** | High |
| **Effort** | 0.5–1 day |
| **Release** | readme.txt fixes: v1.2 · version bumps: v2.0 |
| **Depends on** | — (gates docs 03.2, 05, 06) |

## Current state

- `priority-plus-navigation.php:7-8` declares `Requires at least: 6.0` and `Requires PHP: 7.4`.
- The bootstrap (`priority-plus-navigation.php:26-29`) calls `wp_trigger_error()`, which **only exists since WP 6.4** — the declared 6.0 minimum is already inaccurate. On WP 6.0–6.3 with a missing autoloader this would fatal on an undefined function instead of surfacing the intended error.
- `readme.txt` is **missing both** the `Requires at least:` and `Requires PHP:` headers entirely; the WP.org directory falls back to the plugin header, but the readme is the canonical source and should carry them.
- `readme.txt` FAQ/description claims the default More-button label is **"Browse"**, but the actual default is **"More"** (`src/config.js:7`, `src/variation/block.js` attribute default, `classes/class-block-renderer.php` `collect_attributes()`). Documentation and code have drifted.
- `package.json` pins `@wordpress/scripts ^31.6.0`; current is 32.x. Module builds (needed for doc 03 part 2) require the `--experimental-modules` flag even on 32.x.
- `Tested up to: 7.0` — verify against the actual latest WP release at each ship date.

## Proposed changes

### v1.2 (non-breaking)

1. Add `Requires at least:` and `Requires PHP:` headers to `readme.txt`, matching the plugin header.
2. Correct the declared WP minimum to **6.4** (honest floor given `wp_trigger_error`), or guard the call with `function_exists()` if 6.0 support genuinely matters. Recommendation: declare 6.4 — WP 6.0 is long past its support window.
3. Fix the "Browse" vs "More" drift. Decide the canonical default (recommendation: **"More"**, matching the code and the common Priority+ convention) and align readme.txt FAQ, description, and any screenshots.
4. Upgrade `@wordpress/scripts` to ^32.x. This is a dev-dependency change with no runtime impact; run the full lint/build/format suite after upgrading and fix any new lint findings.

### v2.0 (breaking)

5. Bump to **`Requires at least: 6.8`** and **`Requires PHP: 8.0`**:
- The Interactivity API and Script Modules API ship in WP 6.5, but current core patterns the migration copies (`withSyncEvent`, mature module registration, `wp_register_script_module` ergonomics) make 6.8 the honest floor. It also matches what current Gutenberg targets (Gutenberg 23.5 declares `Requires at least: 6.9`; 6.8 keeps one version of slack).
- PHP 8.0 matches current ecosystem baselines; nothing in the codebase requires 7.4-only syntax. `WP_HTML_Tag_Processor` (doc 02) works on 7.4, so the PHP bump is policy, not necessity — it can be dropped to 7.4 if user data argues for it.
6. State the support policy in readme.txt: sites on WP 6.4–6.7 stay on the 1.x line; 1.x receives security fixes only after 2.0 ships.

## Version discipline going forward

- Plugin header `Version`, readme.txt `Stable tag`, and the `PRIORITY_PLUS_NAVIGATION_VERSION`-style constants (if introduced) must move together; the existing `bump changelog` / `release:` commit pattern should be documented in `CONTRIBUTING` or a release checklist here.
- Re-verify `Tested up to:` on every release.

## Acceptance criteria

- [ ] readme.txt carries `Requires at least`, `Requires PHP`, accurate `Tested up to`, and the corrected default-label copy.
- [ ] Plugin header and readme.txt agree.
- [ ] `npm run build && npm run lint:js && npm run lint:css && composer phpcs` pass on @wordpress/scripts 32.x.
- [ ] v2.0 readme includes the 1.x support-policy paragraph.
71 changes: 71 additions & 0 deletions planned-updates/02-tag-processor-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 02 — Replace Regex HTML Rewriting with WP_HTML_Tag_Processor

| | |
|---|---|
| **Priority** | High |
| **Effort** | 2–3 days |
| **Release** | v1.2 (non-breaking) |
| **Depends on** | — |
| **Prerequisite for** | 05 (directive injection), 06 (server-rendered dropdown) |

## Current state

`classes/class-block-renderer.php` modifies the rendered `core/navigation` output with regular expressions:

- `inject_priority_attributes()` (line 193) builds a `<nav …>` match pattern, **strips any existing `style` attribute** with `preg_replace` (line 220), and appends `data-more-label`, `data-more-icon`, `data-overlay-menu`, `data-mobile-collapse`, and a rebuilt `style` attribute via a second `preg_replace` (line 222).
- `preserve_existing_styles()` (line 254) regex-extracts the current inline style (`preg_match` at line 255) so WP-generated nav styles survive the strip-and-rebuild.

Regex HTML manipulation is fragile against attribute ordering, quoting variants, entities in attribute values, and markup changes from core or other `render_block` filters running earlier. WordPress core stopped doing this: `core/navigation` itself injects its Interactivity directives into saved markup with `WP_HTML_Tag_Processor` (see `block_core_navigation_add_directives_to_submenu()` in Gutenberg's `packages/block-library/src/navigation/index.php` — `next_tag()` / `get_attribute()` / `set_attribute()` / `get_updated_html()`).

## Proposed change

Rewrite the injection layer of `Block_Renderer` on `WP_HTML_Tag_Processor` (available since WP 6.2 — safe for the v1.2 minimum):

```php
private function inject_priority_attributes( string $block_content, array $attributes ): string {
$processor = new WP_HTML_Tag_Processor( $block_content );

if ( ! $processor->next_tag( array( 'tag_name' => 'NAV' ) ) ) {
return $block_content;
}

$processor->set_attribute( 'data-more-label', $attributes['more_label'] );
$processor->set_attribute( 'data-overlay-menu', $attributes['overlay_menu'] );
// … remaining data attributes …

$existing_style = (string) $processor->get_attribute( 'style' );
$processor->set_attribute( 'style', $this->merge_styles( $existing_style, $attributes ) );

return $processor->get_updated_html();
}
```

Notes:

- `preserve_existing_styles()` collapses into a `get_attribute( 'style' )` read — no regex extraction, no strip-and-re-add. The style merge logic (`build_style_parts()`, `add_toggle_styles()`, `add_menu_styles()`) keeps producing the same CSS custom-property strings.
- **`CSS_Converter` is untouched.** It converts values (presets, borders, padding), not markup.
- Optionally scope the `next_tag` to the nav carrying `wp-block-navigation` via `class_name` matching, mirroring the current regex's intent; in practice the first `<nav>` of a `core/navigation` render is the block wrapper.
- No behavior change is intended: same attributes, same style string, same class contract.

## Parity requirement (the real work)

The swap must be provably output-identical, modulo attribute ordering. Build a PHPUnit data provider (lands in the doc 09 test suite) covering:

- nav with / without an existing `style` attribute (WP emits one when the nav has layout/colors)
- each attribute family present and absent: toggle colors, hover colors, padding (flat + per-side), border (flat + per-side), radius (string + per-corner), dropdown styling set, submenu colors
- `overlayMenu` values `never` / `mobile` / `always` (the `always` case must still short-circuit in `is_priority_nav_enabled()`, line 93)
- preset values (`var:preset|spacing|30`) passing through `CSS_Converter::convert_preset_value()`
- markup quirks: single-quoted attributes, entities in `aria-label`, another filter having already added attributes to the nav

Snapshot the regex implementation's output for the matrix **before** the refactor, then assert the Tag Processor implementation matches (normalizing attribute order).

## Risks

- Subtle output diffs where the regex silently tolerated malformed input — the snapshot matrix is the mitigation; treat any diff as a decision point, not an auto-accept.
- `WP_HTML_Tag_Processor::set_attribute()` escapes values; the regex path built raw strings. Verify style strings containing `var()` and semicolons round-trip identically.

## Definition of done

- [ ] No `preg_match` / `preg_replace` remains in `class-block-renderer.php`.
- [ ] Snapshot parity suite green across the attribute matrix.
- [ ] `docs/architecture.md` render-pipeline section updated.
Loading
Loading