Skip to content
Closed
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
1 change: 1 addition & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Bug Fixes

- Notes: Stop deepening an inline note marker's tint on hover, focus, and selection. The tint sits behind the text and is subtracted from whatever contrast the theme provides, so emphasis is now an author-colored underline instead ([#80543](https://github.com/WordPress/gutenberg/issues/80543)).
- `mediaUpload`: Add an `isTransportOnly` parameter, set by the `@wordpress/upload-media` queue, which owns progress tracking and save locking for its own items and uses this function only as its server transport. Fixes the progress snackbar showing "1 of 2" for a single HEIC upload in Safari ([#80369](https://github.com/WordPress/gutenberg/issues/80369)).

## 14.51.0 (2026-07-14)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,31 @@ import { useStyleOverride } from '@wordpress/block-editor';
*/
import { getAvatarBorderColor } from './utils';

// Hex alpha suffixes for the rest / active states. Kept low so the marker
// reads as a soft tint at rest and gets noticeably stronger when focused or
// hovered. (0x40 ≈ 25%, 0x80 ≈ 50%.)
const REST_ALPHA = '40';
const ACTIVE_ALPHA = '80';
/*
* Hex alpha suffix for the tint painted behind the marker's text. One low value
* for every state, on purpose: the tint sits behind the glyphs, so raising it
* eats into whatever text/background contrast the theme already provides, and
* the canvas background comes from `theme.json`, so the composited result
* cannot be measured from CSS. (0x40 ≈ 25%.)
*/
const TINT_ALPHA = '40';

/**
* Emphasis for hover / focus / the selected note. Carried by an opaque
* underline in the author's color rather than a stronger tint, so the marker
* reads more strongly without changing what sits behind the text.
*
* @param {string} color The author's `#RRGGBB` color.
* @return {string} Declarations for the emphasized state.
*/
function emphasis( color ) {
return (
'text-decoration-line:underline;' +
`text-decoration-color:${ color };` +
'text-decoration-thickness:0.125em;' +
'text-underline-offset:0.15em;'
);
}

// Reset the browser's default `<mark>` styling so the per-author rules below
// are what readers actually see (without it, `mark` ships with a bright yellow
Expand All @@ -25,6 +45,10 @@ const BASE_RESET = 'mark.wp-note{background-color:transparent;color:inherit;}';
* Build the CSS rule set that tints each inline-note marker with its author's
* avatar color. Pure helper extracted so it can be unit-tested without React.
*
* The tint stays at a single low alpha in every state and emphasis is expressed
* as an underline, so marking a note can only cost the theme's text contrast
* the one fixed amount, never more when the note is hovered or selected.
*
* @param {Array} threads Unresolved note threads (each with `id` and `author`).
* @param {string|null} selectedId ID of the currently selected note, if any.
* @return {string} A serialized CSS string targeting the in-content note markers.
Expand All @@ -43,14 +67,12 @@ export function buildHighlightCss( threads, selectedId = null ) {
// from stored data.
const escapedId = String( thread.id ).replace( /["\\]/g, '\\$&' );
const sel = `mark.wp-note[data-id="${ escapedId }"]`;
rules.push( `${ sel }{background-color:${ color }${ REST_ALPHA };}` );
rules.push( `${ sel }{background-color:${ color }${ TINT_ALPHA };}` );
rules.push(
`${ sel }:hover,${ sel }:focus-within{background-color:${ color }${ ACTIVE_ALPHA };}`
`${ sel }:hover,${ sel }:focus-within{${ emphasis( color ) }}`
);
if ( selectedId && String( selectedId ) === String( thread.id ) ) {
rules.push(
`${ sel }{background-color:${ color }${ ACTIVE_ALPHA };}`
);
rules.push( `${ sel }{${ emphasis( color ) }}` );
}
}
return rules.join( '' );
Expand All @@ -65,8 +87,8 @@ export function buildHighlightCss( threads, selectedId = null ) {
* Uses `useStyleOverride` so the styles reach the iframed canvas; a plain
* `<style>` element rendered in the sidebar would only affect the parent doc.
*
* Opacity boosts on `:hover`, `:focus-within`, and when the matching thread is
* the editor's selected note.
* An author-colored underline is added on `:hover`, `:focus-within`, and when
* the matching thread is the editor's selected note.
*
* @param {Object} props
* @param {Array} props.threads Unresolved note threads.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ describe( 'buildHighlightCss', () => {
);
} );

it( 'emits a higher-alpha (0x80) rule on hover and focus-within for each thread', () => {
it( 'emphasizes hover and focus-within with an author-colored underline', () => {
const css = buildHighlightCss( [ { id: 7, author: 1 } ] );
const color = getAvatarBorderColor( 1 );
expect( css ).toContain(
`mark.wp-note[data-id="7"]:hover,mark.wp-note[data-id="7"]:focus-within{background-color:${ color }80;}`
`mark.wp-note[data-id="7"]:hover,mark.wp-note[data-id="7"]:focus-within{text-decoration-line:underline;text-decoration-color:${ color };`
);
} );

it( 'boosts opacity for the selected thread by appending a second rule', () => {
it( 'emphasizes the selected thread by appending a second rule', () => {
const css = buildHighlightCss(
[ { id: 7, author: 1 } ],
'7' // selected
Expand All @@ -46,16 +46,40 @@ describe( 'buildHighlightCss', () => {
expect( css ).toContain(
`mark.wp-note[data-id="7"]{background-color:${ color }40;}`
);
// Active rule appended later, so the cascade picks it.
// Emphasis rule appended later, so the cascade picks it.
const restIndex = css.indexOf(
`mark.wp-note[data-id="7"]{background-color:${ color }40;}`
);
const activeIndex = css.lastIndexOf(
`mark.wp-note[data-id="7"]{background-color:${ color }80;}`
`mark.wp-note[data-id="7"]{text-decoration-line:underline;`
);
expect( activeIndex ).toBeGreaterThan( restIndex );
} );

/*
* The tint sits behind the glyphs, so every increment of it is subtracted
* from whatever text/background contrast the theme provides, and CSS cannot
* measure the composited result because the canvas background comes from
* `theme.json`. Emphasis therefore has to come from somewhere other than a
* stronger wash. Guards against reintroducing a per-state alpha.
*/
it( 'never paints a stronger tint behind the text than the rest alpha', () => {
const css = buildHighlightCss(
[
{ id: 7, author: 1 },
{ id: 12, author: 3 },
],
'7'
);
const alphas = [
...css.matchAll( /background-color:#[0-9a-f]{6}([0-9a-f]{2})?/gi ),
]
.map( ( [ , alpha ] ) => alpha )
.filter( Boolean );
expect( alphas.length ).toBeGreaterThan( 0 );
expect( alphas.every( ( alpha ) => alpha === '40' ) ).toBe( true );
} );

it( 'matches numeric and string selectedId variants', () => {
const cssNum = buildHighlightCss( [ { id: 7, author: 1 } ], 7 );
const cssStr = buildHighlightCss( [ { id: 7, author: 1 } ], '7' );
Expand Down
38 changes: 29 additions & 9 deletions test/e2e/specs/editor/various/block-notes.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1327,9 +1327,9 @@ test.describe( 'Block Notes', () => {
const mark = editor.canvas.locator( 'mark.wp-note' ).first();
await expect( mark ).toBeVisible();

// Creating a note auto-selects it, which renders the marker at the
// active opacity. Move focus to the title to deselect so the marker
// settles back to its rest tint.
// Creating a note auto-selects it, which emphasizes the marker with
// an underline. Move focus to the title to deselect so only the
// tint is left to assert on.
await editor.canvas
.getByRole( 'textbox', { name: 'Add title' } )
.click();
Expand Down Expand Up @@ -1594,10 +1594,16 @@ test.describe( 'Block Notes', () => {
await expect( paragraph ).toHaveText( 'Hello brave new world.' );
} );

test( 'boosts the marker opacity when its note is selected', async ( {
test( 'underlines the marker when its note is selected, without deepening the tint', async ( {
editor,
page,
requestUtils,
} ) => {
const me = await requestUtils.rest( { path: '/wp/v2/users/me' } );
const expectedColor =
AVATAR_BORDER_COLORS[ me.id % AVATAR_BORDER_COLORS.length ];
const { r, g, b } = hexToRgb( expectedColor );

await editor.insertBlock( {
name: 'core/paragraph',
attributes: { content: 'Select my note.' },
Expand Down Expand Up @@ -1633,22 +1639,36 @@ test.describe( 'Block Notes', () => {
);
return match && match[ 4 ] ? Number( match[ 4 ] ) : 1;
};
const underlineOf = async () =>
mark.evaluate( ( el ) => {
const style = window.getComputedStyle( el );
return `${ style.textDecorationLine } ${ style.textDecorationColor }`;
} );

// Deselect the freshly added note (focus the title) so the marker
// drops to its rest tint (≈0x40/255).
// settles into its unemphasized state.
await editor.canvas
.getByRole( 'textbox', { name: 'Add title' } )
.click();
await expect.poll( alphaOf ).toBeLessThan( 0.35 );
await expect.poll( underlineOf ).toContain( 'none' );

// Selecting the note from the sidebar promotes its marker to the
// stronger active alpha (≈0x80/255) via the selected-note rule.
// Selecting the note from the sidebar emphasizes its marker with an
// underline in the author's color.
await page
.getByRole( 'region', { name: 'Editor settings' } )
.getByRole( 'treeitem', { name: 'Note: Pick me' } )
.click();

await expect.poll( alphaOf ).toBeGreaterThan( 0.4 );
await expect
.poll( underlineOf )
.toBe( `underline rgb(${ r }, ${ g }, ${ b })` );

/*
* The tint behind the text must not deepen with the emphasis: it is
* subtracted from whatever contrast the theme already provides, and
* CSS cannot measure the composited result. See #80543.
*/
expect( await alphaOf() ).toBeLessThan( 0.35 );
} );
} );

Expand Down
Loading