diff --git a/packages/block-library/src/file/view.js b/packages/block-library/src/file/view.js index 79340223f007cb..62c31b7b365ff2 100644 --- a/packages/block-library/src/file/view.js +++ b/packages/block-library/src/file/view.js @@ -7,10 +7,14 @@ import { store } from '@wordpress/interactivity'; */ import { browserSupportsPdfs } from './utils'; -store( 'core/file', { - state: { - get hasPdfPreview() { - return browserSupportsPdfs(); +store( + 'core/file', + { + state: { + get hasPdfPreview() { + return browserSupportsPdfs(); + }, }, }, -} ); + { lock: true } +); diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index 2d5268e4836cb7..8ae0149726570c 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -77,269 +77,278 @@ function handleScroll( ctx ) { } } -const { state, actions, callbacks } = store( 'core/image', { - state: { - windowWidth: window.innerWidth, - windowHeight: window.innerHeight, - get roleAttribute() { - const ctx = getContext(); - return ctx.lightboxEnabled ? 'dialog' : null; - }, - get ariaModal() { - const ctx = getContext(); - return ctx.lightboxEnabled ? 'true' : null; - }, - get dialogLabel() { - const ctx = getContext(); - return ctx.lightboxEnabled ? ctx.dialogLabel : null; - }, - get lightboxObjectFit() { - const ctx = getContext(); - if ( ctx.initialized ) { - return 'cover'; - } - }, - get enlargedImgSrc() { - const ctx = getContext(); - return ctx.initialized - ? ctx.imageUploadedSrc - : 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; - }, - }, - actions: { - showLightbox( event ) { - const ctx = getContext(); - // We can't initialize the lightbox until the reference - // image is loaded, otherwise the UX is broken. - if ( ! ctx.imageLoaded ) { - return; - } - ctx.initialized = true; - ctx.lastFocusedElement = window.document.activeElement; - ctx.scrollDelta = 0; - ctx.pointerType = event.pointerType; - - ctx.lightboxEnabled = true; - setStyles( ctx, ctx.imageRef ); - - ctx.scrollTopReset = - window.pageYOffset || document.documentElement.scrollTop; - - // In most cases, this value will be 0, but this is included - // in case a user has created a page with horizontal scrolling. - ctx.scrollLeftReset = - window.pageXOffset || document.documentElement.scrollLeft; - - // We define and bind the scroll callback here so - // that we can pass the context and as an argument. - // We may be able to change this in the future if we - // define the scroll callback in the store instead, but - // this approach seems to tbe clearest for now. - scrollCallback = handleScroll.bind( null, ctx ); - - // We need to add a scroll event listener to the window - // here because we are unable to otherwise access it via - // the Interactivity API directives. If we add a native way - // to access the window, we can remove this. - window.addEventListener( 'scroll', scrollCallback, false ); - }, - hideLightbox() { - const ctx = getContext(); - ctx.hideAnimationEnabled = true; - if ( ctx.lightboxEnabled ) { - // We want to wait until the close animation is completed - // before allowing a user to scroll again. The duration of this - // animation is defined in the styles.scss and depends on if the - // animation is 'zoom' or 'fade', but in any case we should wait - // a few milliseconds longer than the duration, otherwise a user - // may scroll too soon and cause the animation to look sloppy. - setTimeout( function () { - window.removeEventListener( 'scroll', scrollCallback ); - // If we don't delay before changing the focus, - // the focus ring will appear on Firefox before - // the image has finished animating, which looks broken. - ctx.lightboxTriggerRef.focus( { - preventScroll: true, - } ); - }, 450 ); - - ctx.lightboxEnabled = false; - } +const { state, actions, callbacks } = store( + 'core/image', + { + state: { + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + get roleAttribute() { + const ctx = getContext(); + return ctx.lightboxEnabled ? 'dialog' : null; + }, + get ariaModal() { + const ctx = getContext(); + return ctx.lightboxEnabled ? 'true' : null; + }, + get dialogLabel() { + const ctx = getContext(); + return ctx.lightboxEnabled ? ctx.dialogLabel : null; + }, + get lightboxObjectFit() { + const ctx = getContext(); + if ( ctx.initialized ) { + return 'cover'; + } + }, + get enlargedImgSrc() { + const ctx = getContext(); + return ctx.initialized + ? ctx.imageUploadedSrc + : 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + }, }, - handleKeydown( event ) { - const ctx = getContext(); - if ( ctx.lightboxEnabled ) { - if ( event.key === 'Tab' || event.keyCode === 9 ) { - // If shift + tab it change the direction - if ( - event.shiftKey && - window.document.activeElement === - ctx.firstFocusableElement - ) { - event.preventDefault(); - ctx.lastFocusableElement.focus(); - } else if ( - ! event.shiftKey && - window.document.activeElement === - ctx.lastFocusableElement - ) { - event.preventDefault(); - ctx.firstFocusableElement.focus(); - } + actions: { + showLightbox( event ) { + const ctx = getContext(); + // We can't initialize the lightbox until the reference + // image is loaded, otherwise the UX is broken. + if ( ! ctx.imageLoaded ) { + return; } + ctx.initialized = true; + ctx.lastFocusedElement = window.document.activeElement; + ctx.scrollDelta = 0; + ctx.pointerType = event.pointerType; + + ctx.lightboxEnabled = true; + setStyles( ctx, ctx.imageRef ); + + ctx.scrollTopReset = + window.pageYOffset || document.documentElement.scrollTop; + + // In most cases, this value will be 0, but this is included + // in case a user has created a page with horizontal scrolling. + ctx.scrollLeftReset = + window.pageXOffset || document.documentElement.scrollLeft; + + // We define and bind the scroll callback here so + // that we can pass the context and as an argument. + // We may be able to change this in the future if we + // define the scroll callback in the store instead, but + // this approach seems to tbe clearest for now. + scrollCallback = handleScroll.bind( null, ctx ); + + // We need to add a scroll event listener to the window + // here because we are unable to otherwise access it via + // the Interactivity API directives. If we add a native way + // to access the window, we can remove this. + window.addEventListener( 'scroll', scrollCallback, false ); + }, + hideLightbox() { + const ctx = getContext(); + ctx.hideAnimationEnabled = true; + if ( ctx.lightboxEnabled ) { + // We want to wait until the close animation is completed + // before allowing a user to scroll again. The duration of this + // animation is defined in the styles.scss and depends on if the + // animation is 'zoom' or 'fade', but in any case we should wait + // a few milliseconds longer than the duration, otherwise a user + // may scroll too soon and cause the animation to look sloppy. + setTimeout( function () { + window.removeEventListener( 'scroll', scrollCallback ); + // If we don't delay before changing the focus, + // the focus ring will appear on Firefox before + // the image has finished animating, which looks broken. + ctx.lightboxTriggerRef.focus( { + preventScroll: true, + } ); + }, 450 ); + + ctx.lightboxEnabled = false; + } + }, + handleKeydown( event ) { + const ctx = getContext(); + if ( ctx.lightboxEnabled ) { + if ( event.key === 'Tab' || event.keyCode === 9 ) { + // If shift + tab it change the direction + if ( + event.shiftKey && + window.document.activeElement === + ctx.firstFocusableElement + ) { + event.preventDefault(); + ctx.lastFocusableElement.focus(); + } else if ( + ! event.shiftKey && + window.document.activeElement === + ctx.lastFocusableElement + ) { + event.preventDefault(); + ctx.firstFocusableElement.focus(); + } + } - if ( event.key === 'Escape' || event.keyCode === 27 ) { - actions.hideLightbox( event ); + if ( event.key === 'Escape' || event.keyCode === 27 ) { + actions.hideLightbox( event ); + } } - } - }, - // This is fired just by lazily loaded - // images on the page, not all images. - handleLoad() { - const ctx = getContext(); - const { ref } = getElement(); - ctx.imageLoaded = true; - ctx.imageCurrentSrc = ref.currentSrc; - callbacks.setButtonStyles(); - }, - handleTouchStart() { - isTouching = true; - }, - handleTouchMove( event ) { - const ctx = getContext(); - // On mobile devices, we want to prevent triggering the - // scroll event because otherwise the page jumps around as - // we reset the scroll position. This also means that closing - // the lightbox requires that a user perform a simple tap. This - // may be changed in the future if we find a better alternative - // to override or reset the scroll position during swipe actions. - if ( ctx.lightboxEnabled ) { - event.preventDefault(); - } - }, - handleTouchEnd() { - // We need to wait a few milliseconds before resetting - // to ensure that pinch to zoom works consistently - // on mobile devices when the lightbox is open. - lastTouchTime = Date.now(); - isTouching = false; - }, - }, - callbacks: { - initOriginImage() { - const ctx = getContext(); - const { ref } = getElement(); - ctx.imageRef = ref; - if ( ref.complete ) { + }, + // This is fired just by lazily loaded + // images on the page, not all images. + handleLoad() { + const ctx = getContext(); + const { ref } = getElement(); ctx.imageLoaded = true; ctx.imageCurrentSrc = ref.currentSrc; - } - }, - initTriggerButton() { - const ctx = getContext(); - const { ref } = getElement(); - ctx.lightboxTriggerRef = ref; - }, - initLightbox() { - const ctx = getContext(); - const { ref } = getElement(); - if ( ctx.lightboxEnabled ) { - const focusableElements = - ref.querySelectorAll( focusableSelectors ); - ctx.firstFocusableElement = focusableElements[ 0 ]; - ctx.lastFocusableElement = - focusableElements[ focusableElements.length - 1 ]; - - // Move focus to the dialog when opening it. - ref.focus(); - } + callbacks.setButtonStyles(); + }, + handleTouchStart() { + isTouching = true; + }, + handleTouchMove( event ) { + const ctx = getContext(); + // On mobile devices, we want to prevent triggering the + // scroll event because otherwise the page jumps around as + // we reset the scroll position. This also means that closing + // the lightbox requires that a user perform a simple tap. This + // may be changed in the future if we find a better alternative + // to override or reset the scroll position during swipe actions. + if ( ctx.lightboxEnabled ) { + event.preventDefault(); + } + }, + handleTouchEnd() { + // We need to wait a few milliseconds before resetting + // to ensure that pinch to zoom works consistently + // on mobile devices when the lightbox is open. + lastTouchTime = Date.now(); + isTouching = false; + }, }, - setButtonStyles() { - const { ref } = getElement(); - const { naturalWidth, naturalHeight, offsetWidth, offsetHeight } = - ref; - - // If the image isn't loaded yet, we can't - // calculate where the button should be. - if ( naturalWidth === 0 || naturalHeight === 0 ) { - return; - } + callbacks: { + initOriginImage() { + const ctx = getContext(); + const { ref } = getElement(); + ctx.imageRef = ref; + if ( ref.complete ) { + ctx.imageLoaded = true; + ctx.imageCurrentSrc = ref.currentSrc; + } + }, + initTriggerButton() { + const ctx = getContext(); + const { ref } = getElement(); + ctx.lightboxTriggerRef = ref; + }, + initLightbox() { + const ctx = getContext(); + const { ref } = getElement(); + if ( ctx.lightboxEnabled ) { + const focusableElements = + ref.querySelectorAll( focusableSelectors ); + ctx.firstFocusableElement = focusableElements[ 0 ]; + ctx.lastFocusableElement = + focusableElements[ focusableElements.length - 1 ]; + + // Move focus to the dialog when opening it. + ref.focus(); + } + }, + setButtonStyles() { + const { ref } = getElement(); + const { + naturalWidth, + naturalHeight, + offsetWidth, + offsetHeight, + } = ref; + + // If the image isn't loaded yet, we can't + // calculate where the button should be. + if ( naturalWidth === 0 || naturalHeight === 0 ) { + return; + } - const figure = ref.parentElement; - const figureWidth = ref.parentElement.clientWidth; - - // We need special handling for the height because - // a caption will cause the figure to be taller than - // the image, which means we need to account for that - // when calculating the placement of the button in the - // top right corner of the image. - let figureHeight = ref.parentElement.clientHeight; - const caption = figure.querySelector( 'figcaption' ); - if ( caption ) { - const captionComputedStyle = window.getComputedStyle( caption ); - if ( - ! [ 'absolute', 'fixed' ].includes( - captionComputedStyle.position - ) - ) { - figureHeight = - figureHeight - - caption.offsetHeight - - parseFloat( captionComputedStyle.marginTop ) - - parseFloat( captionComputedStyle.marginBottom ); + const figure = ref.parentElement; + const figureWidth = ref.parentElement.clientWidth; + + // We need special handling for the height because + // a caption will cause the figure to be taller than + // the image, which means we need to account for that + // when calculating the placement of the button in the + // top right corner of the image. + let figureHeight = ref.parentElement.clientHeight; + const caption = figure.querySelector( 'figcaption' ); + if ( caption ) { + const captionComputedStyle = + window.getComputedStyle( caption ); + if ( + ! [ 'absolute', 'fixed' ].includes( + captionComputedStyle.position + ) + ) { + figureHeight = + figureHeight - + caption.offsetHeight - + parseFloat( captionComputedStyle.marginTop ) - + parseFloat( captionComputedStyle.marginBottom ); + } } - } - const buttonOffsetTop = figureHeight - offsetHeight; - const buttonOffsetRight = figureWidth - offsetWidth; - - const ctx = getContext(); - - // In the case of an image with object-fit: contain, the - // size of the element can be larger than the image itself, - // so we need to calculate where to place the button. - if ( ctx.scaleAttr === 'contain' ) { - // Natural ratio of the image. - const naturalRatio = naturalWidth / naturalHeight; - // Offset ratio of the image. - const offsetRatio = offsetWidth / offsetHeight; - - if ( naturalRatio >= offsetRatio ) { - // If it reaches the width first, keep - // the width and compute the height. - const referenceHeight = offsetWidth / naturalRatio; - ctx.imageButtonTop = - ( offsetHeight - referenceHeight ) / 2 + - buttonOffsetTop + - 16; - ctx.imageButtonRight = buttonOffsetRight + 16; + const buttonOffsetTop = figureHeight - offsetHeight; + const buttonOffsetRight = figureWidth - offsetWidth; + + const ctx = getContext(); + + // In the case of an image with object-fit: contain, the + // size of the element can be larger than the image itself, + // so we need to calculate where to place the button. + if ( ctx.scaleAttr === 'contain' ) { + // Natural ratio of the image. + const naturalRatio = naturalWidth / naturalHeight; + // Offset ratio of the image. + const offsetRatio = offsetWidth / offsetHeight; + + if ( naturalRatio >= offsetRatio ) { + // If it reaches the width first, keep + // the width and compute the height. + const referenceHeight = offsetWidth / naturalRatio; + ctx.imageButtonTop = + ( offsetHeight - referenceHeight ) / 2 + + buttonOffsetTop + + 16; + ctx.imageButtonRight = buttonOffsetRight + 16; + } else { + // If it reaches the height first, keep + // the height and compute the width. + const referenceWidth = offsetHeight * naturalRatio; + ctx.imageButtonTop = buttonOffsetTop + 16; + ctx.imageButtonRight = + ( offsetWidth - referenceWidth ) / 2 + + buttonOffsetRight + + 16; + } } else { - // If it reaches the height first, keep - // the height and compute the width. - const referenceWidth = offsetHeight * naturalRatio; ctx.imageButtonTop = buttonOffsetTop + 16; - ctx.imageButtonRight = - ( offsetWidth - referenceWidth ) / 2 + - buttonOffsetRight + - 16; + ctx.imageButtonRight = buttonOffsetRight + 16; } - } else { - ctx.imageButtonTop = buttonOffsetTop + 16; - ctx.imageButtonRight = buttonOffsetRight + 16; - } - }, - setStylesOnResize() { - const ctx = getContext(); - const { ref } = getElement(); - if ( - ctx.lightboxEnabled && - ( state.windowWidth || state.windowHeight ) - ) { - setStyles( ctx, ref ); - } + }, + setStylesOnResize() { + const ctx = getContext(); + const { ref } = getElement(); + if ( + ctx.lightboxEnabled && + ( state.windowWidth || state.windowHeight ) + ) { + setStyles( ctx, ref ); + } + }, }, }, -} ); + { lock: true } +); window.addEventListener( 'resize', diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index d42832a1f8d02e..eb553eee9ca181 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -23,193 +23,206 @@ const focusableSelectors = [ // capture the clicks, instead of relying on the focusout event. document.addEventListener( 'click', () => {} ); -const { state, actions } = store( 'core/navigation', { - state: { - get roleAttribute() { - const ctx = getContext(); - return ctx.type === 'overlay' && state.isMenuOpen ? 'dialog' : null; - }, - get ariaModal() { - const ctx = getContext(); - return ctx.type === 'overlay' && state.isMenuOpen ? 'true' : null; - }, - get ariaLabel() { - const ctx = getContext(); - return ctx.type === 'overlay' && state.isMenuOpen - ? ctx.ariaLabel - : null; - }, - get isMenuOpen() { - // The menu is opened if either `click`, `hover` or `focus` is true. - return ( - Object.values( state.menuOpenedBy ).filter( Boolean ).length > 0 - ); - }, - get menuOpenedBy() { - const ctx = getContext(); - return ctx.type === 'overlay' - ? ctx.overlayOpenedBy - : ctx.submenuOpenedBy; - }, - }, - actions: { - openMenuOnHover() { - const { type, overlayOpenedBy } = getContext(); - if ( - type === 'submenu' && - // Only open on hover if the overlay is closed. - Object.values( overlayOpenedBy || {} ).filter( Boolean ) - .length === 0 - ) - actions.openMenu( 'hover' ); - }, - closeMenuOnHover() { - actions.closeMenu( 'hover' ); - }, - openMenuOnClick() { - const ctx = getContext(); - const { ref } = getElement(); - ctx.previousFocus = ref; - actions.openMenu( 'click' ); - }, - closeMenuOnClick() { - actions.closeMenu( 'click' ); - actions.closeMenu( 'focus' ); - }, - openMenuOnFocus() { - actions.openMenu( 'focus' ); - }, - toggleMenuOnClick() { - const ctx = getContext(); - const { ref } = getElement(); - // Safari won't send focus to the clicked element, so we need to manually place it: https://bugs.webkit.org/show_bug.cgi?id=22261 - if ( window.document.activeElement !== ref ) ref.focus(); - const { menuOpenedBy } = state; - if ( menuOpenedBy.click || menuOpenedBy.focus ) { - actions.closeMenu( 'click' ); - actions.closeMenu( 'focus' ); - } else { +const { state, actions } = store( + 'core/navigation', + { + state: { + get roleAttribute() { + const ctx = getContext(); + return ctx.type === 'overlay' && state.isMenuOpen + ? 'dialog' + : null; + }, + get ariaModal() { + const ctx = getContext(); + return ctx.type === 'overlay' && state.isMenuOpen + ? 'true' + : null; + }, + get ariaLabel() { + const ctx = getContext(); + return ctx.type === 'overlay' && state.isMenuOpen + ? ctx.ariaLabel + : null; + }, + get isMenuOpen() { + // The menu is opened if either `click`, `hover` or `focus` is true. + return ( + Object.values( state.menuOpenedBy ).filter( Boolean ) + .length > 0 + ); + }, + get menuOpenedBy() { + const ctx = getContext(); + return ctx.type === 'overlay' + ? ctx.overlayOpenedBy + : ctx.submenuOpenedBy; + }, + }, + actions: { + openMenuOnHover() { + const { type, overlayOpenedBy } = getContext(); + if ( + type === 'submenu' && + // Only open on hover if the overlay is closed. + Object.values( overlayOpenedBy || {} ).filter( Boolean ) + .length === 0 + ) + actions.openMenu( 'hover' ); + }, + closeMenuOnHover() { + actions.closeMenu( 'hover' ); + }, + openMenuOnClick() { + const ctx = getContext(); + const { ref } = getElement(); ctx.previousFocus = ref; actions.openMenu( 'click' ); - } - }, - handleMenuKeydown( event ) { - const { type, firstFocusableElement, lastFocusableElement } = - getContext(); - if ( state.menuOpenedBy.click ) { - // If Escape close the menu. - if ( event?.key === 'Escape' ) { + }, + closeMenuOnClick() { + actions.closeMenu( 'click' ); + actions.closeMenu( 'focus' ); + }, + openMenuOnFocus() { + actions.openMenu( 'focus' ); + }, + toggleMenuOnClick() { + const ctx = getContext(); + const { ref } = getElement(); + // Safari won't send focus to the clicked element, so we need to manually place it: https://bugs.webkit.org/show_bug.cgi?id=22261 + if ( window.document.activeElement !== ref ) ref.focus(); + const { menuOpenedBy } = state; + if ( menuOpenedBy.click || menuOpenedBy.focus ) { actions.closeMenu( 'click' ); actions.closeMenu( 'focus' ); - return; + } else { + ctx.previousFocus = ref; + actions.openMenu( 'click' ); } + }, + handleMenuKeydown( event ) { + const { type, firstFocusableElement, lastFocusableElement } = + getContext(); + if ( state.menuOpenedBy.click ) { + // If Escape close the menu. + if ( event?.key === 'Escape' ) { + actions.closeMenu( 'click' ); + actions.closeMenu( 'focus' ); + return; + } - // Trap focus if it is an overlay (main menu). - if ( type === 'overlay' && event.key === 'Tab' ) { - // If shift + tab it change the direction. - if ( - event.shiftKey && - window.document.activeElement === firstFocusableElement - ) { - event.preventDefault(); - lastFocusableElement.focus(); - } else if ( - ! event.shiftKey && - window.document.activeElement === lastFocusableElement - ) { - event.preventDefault(); - firstFocusableElement.focus(); + // Trap focus if it is an overlay (main menu). + if ( type === 'overlay' && event.key === 'Tab' ) { + // If shift + tab it change the direction. + if ( + event.shiftKey && + window.document.activeElement === + firstFocusableElement + ) { + event.preventDefault(); + lastFocusableElement.focus(); + } else if ( + ! event.shiftKey && + window.document.activeElement === + lastFocusableElement + ) { + event.preventDefault(); + firstFocusableElement.focus(); + } } } - } - }, - handleMenuFocusout( event ) { - const { modal } = getContext(); - // If focus is outside modal, and in the document, close menu - // event.target === The element losing focus - // event.relatedTarget === The element receiving focus (if any) - // When focusout is outsite the document, - // `window.document.activeElement` doesn't change. + }, + handleMenuFocusout( event ) { + const { modal } = getContext(); + // If focus is outside modal, and in the document, close menu + // event.target === The element losing focus + // event.relatedTarget === The element receiving focus (if any) + // When focusout is outsite the document, + // `window.document.activeElement` doesn't change. - // The event.relatedTarget is null when something outside the navigation menu is clicked. This is only necessary for Safari. - if ( - event.relatedTarget === null || - ( ! modal?.contains( event.relatedTarget ) && - event.target !== window.document.activeElement ) - ) { - actions.closeMenu( 'click' ); - actions.closeMenu( 'focus' ); - } - }, + // The event.relatedTarget is null when something outside the navigation menu is clicked. This is only necessary for Safari. + if ( + event.relatedTarget === null || + ( ! modal?.contains( event.relatedTarget ) && + event.target !== window.document.activeElement ) + ) { + actions.closeMenu( 'click' ); + actions.closeMenu( 'focus' ); + } + }, - openMenu( menuOpenedOn = 'click' ) { - const { type } = getContext(); - state.menuOpenedBy[ menuOpenedOn ] = true; - if ( type === 'overlay' ) { - // Add a `has-modal-open` class to the root. - document.documentElement.classList.add( 'has-modal-open' ); - } - }, + openMenu( menuOpenedOn = 'click' ) { + const { type } = getContext(); + state.menuOpenedBy[ menuOpenedOn ] = true; + if ( type === 'overlay' ) { + // Add a `has-modal-open` class to the root. + document.documentElement.classList.add( 'has-modal-open' ); + } + }, - closeMenu( menuClosedOn = 'click' ) { - const ctx = getContext(); - state.menuOpenedBy[ menuClosedOn ] = false; - // Check if the menu is still open or not. - if ( ! state.isMenuOpen ) { - if ( ctx.modal?.contains( window.document.activeElement ) ) { - ctx.previousFocus?.focus(); + closeMenu( menuClosedOn = 'click' ) { + const ctx = getContext(); + state.menuOpenedBy[ menuClosedOn ] = false; + // Check if the menu is still open or not. + if ( ! state.isMenuOpen ) { + if ( + ctx.modal?.contains( window.document.activeElement ) + ) { + ctx.previousFocus?.focus(); + } + ctx.modal = null; + ctx.previousFocus = null; + if ( ctx.type === 'overlay' ) { + document.documentElement.classList.remove( + 'has-modal-open' + ); + } } - ctx.modal = null; - ctx.previousFocus = null; - if ( ctx.type === 'overlay' ) { - document.documentElement.classList.remove( - 'has-modal-open' - ); + }, + }, + callbacks: { + initMenu() { + const ctx = getContext(); + const { ref } = getElement(); + if ( state.isMenuOpen ) { + const focusableElements = + ref.querySelectorAll( focusableSelectors ); + ctx.modal = ref; + ctx.firstFocusableElement = focusableElements[ 0 ]; + ctx.lastFocusableElement = + focusableElements[ focusableElements.length - 1 ]; } - } - }, - }, - callbacks: { - initMenu() { - const ctx = getContext(); - const { ref } = getElement(); - if ( state.isMenuOpen ) { - const focusableElements = - ref.querySelectorAll( focusableSelectors ); - ctx.modal = ref; - ctx.firstFocusableElement = focusableElements[ 0 ]; - ctx.lastFocusableElement = - focusableElements[ focusableElements.length - 1 ]; - } - }, - focusFirstElement() { - const { ref } = getElement(); - if ( state.isMenuOpen ) { - const focusableElements = - ref.querySelectorAll( focusableSelectors ); - focusableElements?.[ 0 ]?.focus(); - } - }, - initNav() { - const context = getContext(); - const mediaQuery = window.matchMedia( - `(max-width: ${ NAVIGATION_MOBILE_COLLAPSE })` - ); + }, + focusFirstElement() { + const { ref } = getElement(); + if ( state.isMenuOpen ) { + const focusableElements = + ref.querySelectorAll( focusableSelectors ); + focusableElements?.[ 0 ]?.focus(); + } + }, + initNav() { + const context = getContext(); + const mediaQuery = window.matchMedia( + `(max-width: ${ NAVIGATION_MOBILE_COLLAPSE })` + ); - // Run once to set the initial state. - context.isCollapsed = mediaQuery.matches; + // Run once to set the initial state. + context.isCollapsed = mediaQuery.matches; - function handleCollapse( event ) { - context.isCollapsed = event.matches; - } + function handleCollapse( event ) { + context.isCollapsed = event.matches; + } - // Run on resize to update the state. - mediaQuery.addEventListener( 'change', handleCollapse ); + // Run on resize to update the state. + mediaQuery.addEventListener( 'change', handleCollapse ); - // Remove the listener when the component is unmounted. - return () => { - mediaQuery.removeEventListener( 'change', handleCollapse ); - }; + // Remove the listener when the component is unmounted. + return () => { + mediaQuery.removeEventListener( 'change', handleCollapse ); + }; + }, }, }, -} ); + { lock: true } +); diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index ee811b4b8e90f1..396bf4de643698 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -18,54 +18,62 @@ const isValidEvent = ( event ) => ! event.shiftKey && ! event.defaultPrevented; -store( 'core/query', { - actions: { - *navigate( event ) { - const ctx = getContext(); - const { ref } = getElement(); - const queryRef = ref.closest( - '.wp-block-query[data-wp-router-region]' - ); - const isDisabled = queryRef?.dataset.wpNavigationDisabled; +store( + 'core/query', + { + actions: { + *navigate( event ) { + const ctx = getContext(); + const { ref } = getElement(); + const queryRef = ref.closest( + '.wp-block-query[data-wp-router-region]' + ); + const isDisabled = queryRef?.dataset.wpNavigationDisabled; - if ( isValidLink( ref ) && isValidEvent( event ) && ! isDisabled ) { - event.preventDefault(); + if ( + isValidLink( ref ) && + isValidEvent( event ) && + ! isDisabled + ) { + event.preventDefault(); - const { actions } = yield import( - '@wordpress/interactivity-router' - ); - yield actions.navigate( ref.href ); - ctx.url = ref.href; + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.navigate( ref.href ); + ctx.url = ref.href; - // Focus the first anchor of the Query block. - const firstAnchor = `.wp-block-post-template a[href]`; - queryRef.querySelector( firstAnchor )?.focus(); - } - }, - *prefetch() { - const { ref } = getElement(); - const queryRef = ref.closest( - '.wp-block-query[data-wp-router-region]' - ); - const isDisabled = queryRef?.dataset.wpNavigationDisabled; - if ( isValidLink( ref ) && ! isDisabled ) { - const { actions } = yield import( - '@wordpress/interactivity-router' + // Focus the first anchor of the Query block. + const firstAnchor = `.wp-block-post-template a[href]`; + queryRef.querySelector( firstAnchor )?.focus(); + } + }, + *prefetch() { + const { ref } = getElement(); + const queryRef = ref.closest( + '.wp-block-query[data-wp-router-region]' ); - yield actions.prefetch( ref.href ); - } + const isDisabled = queryRef?.dataset.wpNavigationDisabled; + if ( isValidLink( ref ) && ! isDisabled ) { + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.prefetch( ref.href ); + } + }, }, - }, - callbacks: { - *prefetch() { - const { url } = getContext(); - const { ref } = getElement(); - if ( url && isValidLink( ref ) ) { - const { actions } = yield import( - '@wordpress/interactivity-router' - ); - yield actions.prefetch( ref.href ); - } + callbacks: { + *prefetch() { + const { url } = getContext(); + const { ref } = getElement(); + if ( url && isValidLink( ref ) ) { + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.prefetch( ref.href ); + } + }, }, }, -} ); + { lock: true } +); diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index b633bf971f363a..0e4c462a2e3213 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -3,66 +3,70 @@ */ import { store, getContext, getElement } from '@wordpress/interactivity'; -const { actions } = store( 'core/search', { - state: { - get ariaLabel() { - const { - isSearchInputVisible, - ariaLabelCollapsed, - ariaLabelExpanded, - } = getContext(); - return isSearchInputVisible - ? ariaLabelExpanded - : ariaLabelCollapsed; +const { actions } = store( + 'core/search', + { + state: { + get ariaLabel() { + const { + isSearchInputVisible, + ariaLabelCollapsed, + ariaLabelExpanded, + } = getContext(); + return isSearchInputVisible + ? ariaLabelExpanded + : ariaLabelCollapsed; + }, + get ariaControls() { + const { isSearchInputVisible, inputId } = getContext(); + return isSearchInputVisible ? null : inputId; + }, + get type() { + const { isSearchInputVisible } = getContext(); + return isSearchInputVisible ? 'submit' : 'button'; + }, + get tabindex() { + const { isSearchInputVisible } = getContext(); + return isSearchInputVisible ? '0' : '-1'; + }, }, - get ariaControls() { - const { isSearchInputVisible, inputId } = getContext(); - return isSearchInputVisible ? null : inputId; - }, - get type() { - const { isSearchInputVisible } = getContext(); - return isSearchInputVisible ? 'submit' : 'button'; - }, - get tabindex() { - const { isSearchInputVisible } = getContext(); - return isSearchInputVisible ? '0' : '-1'; - }, - }, - actions: { - openSearchInput( event ) { - const ctx = getContext(); - const { ref } = getElement(); - if ( ! ctx.isSearchInputVisible ) { - event.preventDefault(); - ctx.isSearchInputVisible = true; - ref.parentElement.querySelector( 'input' ).focus(); - } - }, - closeSearchInput() { - const ctx = getContext(); - ctx.isSearchInputVisible = false; - }, - handleSearchKeydown( event ) { - const { ref } = getElement(); - // If Escape close the menu. - if ( event?.key === 'Escape' ) { - actions.closeSearchInput(); - ref.querySelector( 'button' ).focus(); - } - }, - handleSearchFocusout( event ) { - const { ref } = getElement(); - // If focus is outside search form, and in the document, close menu - // event.target === The element losing focus - // event.relatedTarget === The element receiving focus (if any) - // When focusout is outside the document, - // `window.document.activeElement` doesn't change. - if ( - ! ref.contains( event.relatedTarget ) && - event.target !== window.document.activeElement - ) { - actions.closeSearchInput(); - } + actions: { + openSearchInput( event ) { + const ctx = getContext(); + const { ref } = getElement(); + if ( ! ctx.isSearchInputVisible ) { + event.preventDefault(); + ctx.isSearchInputVisible = true; + ref.parentElement.querySelector( 'input' ).focus(); + } + }, + closeSearchInput() { + const ctx = getContext(); + ctx.isSearchInputVisible = false; + }, + handleSearchKeydown( event ) { + const { ref } = getElement(); + // If Escape close the menu. + if ( event?.key === 'Escape' ) { + actions.closeSearchInput(); + ref.querySelector( 'button' ).focus(); + } + }, + handleSearchFocusout( event ) { + const { ref } = getElement(); + // If focus is outside search form, and in the document, close menu + // event.target === The element losing focus + // event.relatedTarget === The element receiving focus (if any) + // When focusout is outside the document, + // `window.document.activeElement` doesn't change. + if ( + ! ref.contains( event.relatedTarget ) && + event.target !== window.document.activeElement + ) { + actions.closeSearchInput(); + } + }, }, }, -} ); + { lock: true } +);