-
Notifications
You must be signed in to change notification settings - Fork 41
Slide a shared highlight between Add Fields buttons on hover #3246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| /** | ||
| * Field list hover pill. | ||
| * | ||
| * One shared element slides to whichever field button is hovered. The per-button hover in | ||
| * _insert-fields.scss remains as the no-JS fallback. | ||
| * | ||
| * @since x.x | ||
| */ | ||
|
|
||
| const PILL_CLASS = 'frm-insert-fields-hover'; | ||
| const ACTIVE_CLASS = 'frm-has-hover-pill'; | ||
| const ANIMATING_CLASS = 'frm-animating'; | ||
|
|
||
| /** | ||
| * Travel time bounds, in milliseconds. | ||
| */ | ||
| const MIN_TRAVEL = 120; | ||
| const MAX_TRAVEL = 300; | ||
| const TRAVEL_PER_PX = 0.9; | ||
|
|
||
| /** | ||
| * Travels at or above this dim while moving. Shorter hops keep full opacity. | ||
| * | ||
| * @type {number} | ||
| */ | ||
| const DIM_ABOVE_TRAVEL = 200; | ||
|
|
||
| /** | ||
| * Grace period on the fallback un-dim timer, in milliseconds. | ||
| * | ||
| * @type {number} | ||
| */ | ||
| const ANIMATING_GRACE = 60; | ||
|
|
||
| /** | ||
| * Adds the sliding hover pill to every Add Fields list. | ||
| * | ||
| * Basic, Pricing and Advanced are separate lists, and each needs its own pill because positions | ||
| * are measured against the list the button sits in. | ||
| * | ||
| * @since x.x | ||
| * | ||
| * @return {void} | ||
| */ | ||
| export function initFieldListHoverPill() { | ||
| document.querySelectorAll( '#frm-insert-fields .field_type_list' ).forEach( addPillTo ); | ||
| } | ||
|
|
||
| /** | ||
| * Adds a pill to a single field list. | ||
| * | ||
| * @since x.x | ||
| * | ||
| * @param {HTMLElement} list The field list. | ||
| * @return {void} | ||
| */ | ||
| function addPillTo( list ) { | ||
| if ( list.classList.contains( ACTIVE_CLASS ) ) { | ||
| return; | ||
| } | ||
|
|
||
| const pill = document.createElement( 'div' ); | ||
| pill.classList.add( PILL_CLASS, 'frm_hidden' ); | ||
| list.append( pill ); | ||
| list.classList.add( ACTIVE_CLASS ); | ||
|
|
||
| let animatingTimeout; | ||
| let currentX = null; | ||
| let currentY = null; | ||
|
|
||
| /** | ||
| * Ends the dim on transitionend rather than a fixed delay, so a fast sweep across several | ||
| * cards cannot leave the pill stuck dimmed. | ||
| * | ||
| * @return {void} | ||
| */ | ||
| const settle = () => { | ||
| clearTimeout( animatingTimeout ); | ||
| pill.classList.remove( ANIMATING_CLASS ); | ||
| }; | ||
|
|
||
| pill.addEventListener( 'transitionend', event => { | ||
| if ( 'transform' === event.propertyName ) { | ||
| settle(); | ||
| } | ||
| } ); | ||
|
|
||
| /** | ||
| * Moves the pill over a field button, matching its size and position. | ||
| * | ||
| * @param {HTMLElement} button The hovered anchor. | ||
| * @return {void} | ||
| */ | ||
| const moveTo = button => { | ||
| const listRect = list.getBoundingClientRect(); | ||
| const buttonRect = button.getBoundingClientRect(); | ||
| const x = buttonRect.left - listRect.left; | ||
| const y = buttonRect.top - listRect.top; | ||
| const isFirstShow = null === currentX; | ||
| const distance = isFirstShow ? 0 : Math.hypot( x - currentX, y - currentY ); | ||
| const travel = Math.min( MAX_TRAVEL, Math.max( MIN_TRAVEL, Math.round( distance * TRAVEL_PER_PX ) ) ); | ||
|
|
||
| currentX = x; | ||
| currentY = y; | ||
|
|
||
| pill.style.setProperty( '--frm-pill-travel', `${ travel }ms` ); | ||
| pill.style.width = `${ buttonRect.width }px`; | ||
| pill.style.height = `${ buttonRect.height }px`; | ||
| pill.style.transform = `translate(${ x }px, ${ y }px)`; | ||
|
|
||
| pill.classList.remove( 'frm_hidden' ); | ||
|
|
||
| if ( isFirstShow || travel < DIM_ABOVE_TRAVEL ) { | ||
| settle(); | ||
| return; | ||
| } | ||
|
|
||
| pill.classList.add( ANIMATING_CLASS ); | ||
|
|
||
| // Fallback for when transitionend does not fire, e.g. an interrupted or zero-length travel. | ||
| clearTimeout( animatingTimeout ); | ||
| animatingTimeout = setTimeout( settle, travel + ANIMATING_GRACE ); | ||
| }; | ||
|
|
||
| /** | ||
| * Hides the pill. | ||
| * | ||
| * @return {void} | ||
| */ | ||
| const hide = () => { | ||
| settle(); | ||
| pill.classList.add( 'frm_hidden' ); | ||
|
|
||
| // Hidden means display:none, so the next appearance jumps. Forgetting the position | ||
| // stops that jump being measured as a long travel. | ||
| currentX = null; | ||
| currentY = null; | ||
| }; | ||
|
|
||
| // Delegated so field buttons added after load are covered too. | ||
| list.addEventListener( 'mouseover', event => { | ||
| const button = event.target.closest( 'li.frmbutton > a' ); | ||
|
|
||
| if ( ! button || button.classList.contains( 'disabled' ) ) { | ||
| hide(); | ||
| return; | ||
| } | ||
|
|
||
| moveTo( button ); | ||
| } ); | ||
|
|
||
| list.addEventListener( 'mouseleave', hide ); | ||
|
|
||
| // Dragging a field lifts it away from the cursor, leaving the pill with nothing to sit under. | ||
| list.addEventListener( 'mousedown', hide ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -241,3 +241,59 @@ input[disabled] { | |
| .frmbutton.frm_at_limit { | ||
| opacity: 0.5; | ||
| } | ||
|
|
||
| /** | ||
| * Hover pill | ||
| * | ||
| * Created by initFieldListHoverPill(), which also adds .frm-has-hover-pill. Without it the | ||
| * per-button hover above still applies. | ||
| */ | ||
| .field_type_list { | ||
| position: relative; | ||
| } | ||
|
|
||
| .frm-insert-fields-hover { | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| z-index: 0; | ||
| border-radius: var(--small-radius); | ||
| background: #fff; | ||
| box-shadow: var(--box-shadow-md); | ||
| pointer-events: none; | ||
| will-change: transform; | ||
|
|
||
| /* Set per move by initFieldListHoverPill(), scaled to the distance. */ | ||
| --frm-pill-travel: 300ms; | ||
|
|
||
| transition: | ||
| 0.15s opacity ease, | ||
| var(--frm-pill-travel) transform cubic-bezier(0.165, 0.84, 0.44, 1), | ||
| var(--frm-pill-travel) width cubic-bezier(0.165, 0.84, 0.44, 1), | ||
| var(--frm-pill-travel) height cubic-bezier(0.165, 0.84, 0.44, 1); | ||
| } | ||
|
|
||
| .frm-insert-fields-hover.frm-animating { | ||
| opacity: 0.45; | ||
| } | ||
|
|
||
| .field_type_list.frm-has-hover-pill { | ||
|
|
||
| li.frmbutton a { | ||
| position: relative; | ||
| z-index: 1; | ||
| } | ||
|
|
||
| /* The pill paints the hover state now, so the button stops painting its own. */ | ||
| li.frmbutton a:not(.disabled):hover { | ||
| background: transparent; | ||
| box-shadow: none; | ||
| } | ||
|
Comment on lines
+287
to
+291
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Increase the hover override selector specificity. Line 288 cannot override the fallback rule at lines 83-90. The fallback selector includes Proposed fix-.field_type_list.frm-has-hover-pill {
+#frm-insert-fields .field_type_list.frm-has-hover-pill {🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @media (prefers-reduced-motion: reduce) { | ||
|
|
||
| .frm-insert-fields-hover { | ||
| transition: none; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.