Skip to content

Commit

Permalink
Experiment: Add lightbox to Image block using directives (reopened) (#…
Browse files Browse the repository at this point in the history
…50373)

* Add behaviors to the core theme.json

* Add behaviors to json schemas for theme.json

* Add a behaviors panel

* Remove the changes to theme.json schema

* Add behaviors to the VALID_SETTINGS in class-wp-theme-json-gutenberg.php

* Add the first (still broken) version of the lightbox settings

* WIP: Added a SelectControl for the behaviors

* Format PHP

* Format correctly again

* Use the props.name when getting the behaviors

* Add initial e2e tests

* Update the withBehaviors description

* Cleaned up behaviors.js

* Update e2e tests to check visibility

* Update core-blocks doc with behaviors attribute

* Update fixtures with new lightbox attribute

* Add a new theme for e2e tests

* Change theme.json to include `settings.behaviors`

* Remove default behavior value from block.json

* Minor fix to the implementation

* Add more e2e tests

* Create a selector for behaviors

* Add a filter to load behaviors on the server from the theme.json

* Add behaviors on the top-level in core theme.json

* Use the behaviors in the hooks

* Update the comment

* Update the comment in behaviors.js

* Update fixtures and e2e

* Prevent mobile test gutenberg error (temporary - not the best solution)

* Add priority on filter

* Fix php standards

* Found a much better way to fix php tests

* Small refactor

* Add `behaviors` as an allowed key to BLOCK_EDITOR_SETTINGS

* Move the behaviors to top level in the e2e test theme.json file

* Rename the `behaviors` setting to `behaviorsUIEnabled`

* Change "None" to "No behaviors"

* Behaviors ->  behavior

* Fix redundant ternary

* Improve the JSDoc for behaviors selector

* Rename the test themes to make more sense

* Remove definition of `behaviors` attribute in core/image

* Change default value for `behaviors.lightbox` to false and update e2e tests accordingly

* Change the way we get the data from `theme.json` and adjust e2e

* Capitalize behaviors' labels

* Move PHP code adding `theme.json` behaviors to `block-editor-settings`

* Update comment

* Remove the behaviors require from load.php

* Revert "Update comment"

This reverts commit fc812bc6c04e8b3c5c7fe3450c1e39d1dd6f6f3b.

* Revert "Move PHP code adding `theme.json` behaviors to `block-editor-settings`"

This reverts commit e8d16c8bb4977d1e27b758a067066a78fc21f794.

* Remove the comment that was added previously

* Update comments in `behaviors.php`

* Add back the require_once in load.php

* Use `settings.blocks.core/image.behaviors.lightbox`

* Use `behaviors.blocks.core/image.lightbox`

* Remove experimental setting for interactivity API

* Add lightbox to image block

First pass at adding lightbox.

Note: Added custom implementation of Preact Portal
because the children[0] declaration in the render method
was erroneous and undefined, preventing it from working
as expected.

* Add logic for hiding lightbox on esc key press and overlay click

* Improve styles and add note to add conditional for lightbox markup

* Add editor UI and attribute for toggling lightbox

* Remove image translation animation and add fade instead

* Add accessibility; clean up styles; fix bug regarding ref in directives

Cleaned up the PHP file a bit as I implemented standard accessibility
for a lightbox.

I changed names of attributes and properties to be clearer, as well as
conslidated some CSS.

Important to note: I fixed a bug with the directives wherein 'ref'
was coming in as null due to how it was being passed in hooks.js.

* Configure image to use new Interactivity API runtime included in Gutenberg

* Remove viewScript from image config

* Add Portal directive to Interactivity API runtime

* Set scrim to site background color

* Remove extraneous image CSS declaration

* Improve aria labeling

* Code cleanup; simplify syntax, consolidate code

* Refactor code, remove event listeners, consolidate logic

* Fix formatting in SCSS file

* Change CheckboxControl to a ToggleControl; update API docs

* Update wp_enqueue_script to correctly add interactivity runtime

* Fix linter errors

* Update to use core.image namespace

* Pause closing of lightbox slightly when using the mousewheel

* Rename portal directive to 'wp-body' and remove unused reference

* Add internal dependencies flag; update comment

* Remove extraneous code

* Fix accessibility bug due to directives being defined incorrectly

* Move enableLightbox declaration in block.json

* Remove extraneous package.json declarations

* Fix PHP indentation linter error

* Revise package-lock.json to match trunk

* Fix linter errors in portals.js

* Update fixtures

* Tests: Add e2e specs for Image block on the frontend

* Disable lightbox by default

* Add aria-role attribute to lightbox markup

* Add tests

* Update fixtures

* Fix accessibility issues

1.) Changed 'aria-role' attribute to 'role'
2.) Moved lightbox close button to before modal content
3.) Remove logic for closing dialog with Tab key
4.) Move <img> outside of <button> element
5.) Add aria-labelledby attribute to dialog

* Add focus trap

* Label elements as 'inert' when lightbox is open

* Update import of image interactivity scripts

* Fix bug where interactivity only worked with admin enabled

* Put lightbox rendering behind experimental flag

* Add check for query selector and update tests

* Update spacing in PHP file

* Remove tick() and modify CSS

* Update config based on behaviors UI and trunk changes

* Remove extraneous code

* Remove extraneous code and simplify variable name

* Use core WordPress API for experiments flag

* Clean up declarations

* Fix PHP spacing

* Remove `enableLightbox`

* Remove defaultValue from image block.json

* Update render_callback of core/image to use Behaviors UI

* Fix e2e test:
- Remove trailing `}`
- Incorrectly assuming `"lightbox": true`

* Remove 'inert' declarations for now

* Add period to behaviors help text

* Create selector for 'role' attribute

* Update fixtures

* Update comment

* Clean up data-wp-island attribute

* Readd mistakenly deleted line

* Improve syntax of tests

* Replace getByLabel() with getByRole() in some tests

* Remove 'experiments' flag

* Remove dependency on 'experiments' flag in tests

* Revert "Remove dependency on 'experiments' flag in tests"

This reverts commit 429ebbe.

* Revert "Remove 'experiments' flag"

This reverts commit cd0c9dd.

* Change `aria-label` separator

* Add back the experimental setting

---------

Co-authored-by: Michal Czaplinski <mmczaplinski@gmail.com>
Co-authored-by: Carlos Bravo <carlos.bravo@automattic.com>
Co-authored-by: Grzegorz Ziolkowski <grzegorz@gziolo.pl>
Co-authored-by: Mario Santos <santosguillamot@gmail.com>
Co-authored-by: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com>
  • Loading branch information
6 people authored May 24, 2023
1 parent 03751d9 commit ac13b94
Show file tree
Hide file tree
Showing 9 changed files with 607 additions and 5 deletions.
4 changes: 2 additions & 2 deletions lib/experimental/interactivity-api/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,15 @@ function gutenberg_block_core_navigation_add_directives_to_submenu( $w ) {
add_filter( 'render_block_core/navigation', 'gutenberg_block_core_navigation_add_directives_to_markup', 10, 1 );

/**
* Replaces view script for the File and Navigation blocks with version using Interactivity API.
* Replaces view script for the File, Navigation, and Image blocks with version using Interactivity API.
*
* @param array $metadata Block metadata as read in via block.json.
*
* @return array Filtered block type metadata.
*/
function gutenberg_block_update_interactive_view_script( $metadata ) {
if (
in_array( $metadata['name'], array( 'core/file', 'core/navigation' ), true ) &&
in_array( $metadata['name'], array( 'core/file', 'core/navigation', 'core/image' ), true ) &&
str_contains( $metadata['file'], 'build/block-library/blocks' )
) {
$metadata['viewScript'] = array( 'file:./interactivity.min.js' );
Expand Down
2 changes: 1 addition & 1 deletion packages/block-editor/src/hooks/behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const withBehaviors = createHigherOrderComponent( ( BlockEdit ) => {
} );
} }
hideCancelButton={ true }
help={ __( 'Add behaviors' ) }
help={ __( 'Add behaviors.' ) }
size="__unstable-large"
/>
</InspectorControls>
Expand Down
77 changes: 75 additions & 2 deletions packages/block-library/src/image/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* @return string Returns the block content with the data-id attribute added.
*/
function render_block_core_image( $attributes, $content ) {

$processor = new WP_HTML_Tag_Processor( $content );
$processor->next_tag( 'img' );

Expand All @@ -27,16 +28,88 @@ function render_block_core_image( $attributes, $content ) {
// which now wraps Image Blocks within innerBlocks.
// The data-id attribute is added in a core/gallery `render_block_data` hook.
$processor->set_attribute( 'data-id', $attributes['data-id'] );
}

$link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none';

// Get the lightbox setting from the block attributes.
if ( isset( $attributes['behaviors']['lightbox'] ) ) {
$lightbox = $attributes['behaviors']['lightbox'];
// If the lightbox setting is not set in the block attributes, get it from the theme.json file.
} else {
$theme_data = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()->get_data();
if ( isset( $theme_data['behaviors']['blocks']['core/image']['lightbox'] ) ) {
$lightbox = $theme_data['behaviors']['blocks']['core/image']['lightbox'];
} else {
$lightbox = false;
}
}

$experiments = get_option( 'gutenberg-experiments' );

if ( ! empty( $experiments['gutenberg-interactivity-api-core-blocks'] ) && 'none' === $link_destination && $lightbox ) {

$aria_label = 'Open image lightbox';
if ( $processor->get_attribute( 'alt' ) ) {
$aria_label .= ' : ' . $processor->get_attribute( 'alt' );
}
$content = $processor->get_updated_html();

// Wrap the image in the body content with a button.
$img = null;
preg_match( '/<img[^>]+>/', $content, $img );
$button = '<div class="img-container">
<button aria-haspopup="dialog" aria-label="' . $aria_label . '" data-wp-on.click="actions.core.image.showLightbox"></button>'
. $img[0] .
'</div>';
$body_content = preg_replace( '/<img[^>]+>/', $button, $content );

// For the modal, set an ID on the image to be used for an aria-labelledby attribute.
$modal_content = new WP_HTML_Tag_Processor( $content );
$modal_content->next_tag( 'img' );
$image_lightbox_id = $modal_content->get_attribute( 'class' ) . '-lightbox';
$modal_content->set_attribute( 'id', $image_lightbox_id );
$modal_content = $modal_content->get_updated_html();

$background_color = wp_get_global_styles( array( 'color', 'background' ) );
$close_button_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="30" height="30" aria-hidden="true" focusable="false"><path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>';

return
<<<HTML
<div class="wp-lightbox-container"
data-wp-island
data-wp-context='{ "core": { "image": { "initialized": false, "lightboxEnabled": false } } }'>
$body_content
<div data-wp-body="" class="wp-lightbox-overlay"
data-wp-bind.role="selectors.core.image.roleAttribute"
aria-labelledby="$image_lightbox_id"
data-wp-class.initialized="context.core.image.initialized"
data-wp-class.active="context.core.image.lightboxEnabled"
data-wp-bind.aria-hidden="!context.core.image.lightboxEnabled"
data-wp-bind.aria-modal="context.core.image.lightboxEnabled"
data-wp-effect="effects.core.image.initLightbox"
data-wp-on.keydown="actions.core.image.handleKeydown"
data-wp-on.mousewheel="actions.core.image.hideLightbox"
data-wp-on.click="actions.core.image.hideLightbox"
>
<button aria-label="Close lightbox" class="close-button" data-wp-on.click="actions.core.image.hideLightbox">
$close_button_icon
</button>
$modal_content
<div class="scrim" style="background-color: $background_color"></div>
</div>
</div>
HTML;
}
return $content;
}

return $processor->get_updated_html();
}

/**
* Registers the `core/image` block on server.
*/
function register_block_core_image() {

register_block_type_from_metadata(
__DIR__ . '/image',
array(
Expand Down
113 changes: 113 additions & 0 deletions packages/block-library/src/image/interactivity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Internal dependencies
*/
import { store } from '../utils/interactivity';

const focusableSelectors = [
'a[href]',
'area[href]',
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
'select:not([disabled]):not([aria-hidden])',
'textarea:not([disabled]):not([aria-hidden])',
'button:not([disabled]):not([aria-hidden])',
'iframe',
'object',
'embed',
'[contenteditable]',
'[tabindex]:not([tabindex^="-"])',
];

store( {
actions: {
core: {
image: {
showLightbox: ( { context } ) => {
context.core.image.initialized = true;
context.core.image.lightboxEnabled = true;
context.core.image.lastFocusedElement =
window.document.activeElement;
context.core.image.scrollPosition = window.scrollY;
document.documentElement.classList.add(
'has-lightbox-open'
);
},
hideLightbox: async ( { context, event } ) => {
if ( context.core.image.lightboxEnabled ) {
// If scrolling, wait a moment before closing the lightbox.
if (
event.type === 'mousewheel' &&
Math.abs(
window.scrollY -
context.core.image.scrollPosition
) < 5
) {
return;
}
document.documentElement.classList.remove(
'has-lightbox-open'
);

context.core.image.lightboxEnabled = false;
context.core.image.lastFocusedElement.focus();
}
},
handleKeydown: ( { context, actions, event } ) => {
if ( context.core.image.lightboxEnabled ) {
if ( event.key === 'Tab' || event.keyCode === 9 ) {
// If shift + tab it change the direction
if (
event.shiftKey &&
window.document.activeElement ===
context.core.image.firstFocusableElement
) {
event.preventDefault();
context.core.image.lastFocusableElement.focus();
} else if (
! event.shiftKey &&
window.document.activeElement ===
context.core.image.lastFocusableElement
) {
event.preventDefault();
context.core.image.firstFocusableElement.focus();
}
}

if ( event.key === 'Escape' || event.keyCode === 27 ) {
actions.core.image.hideLightbox( {
context,
event,
} );
}
}
},
},
},
},
selectors: {
core: {
image: {
roleAttribute: ( { context } ) => {
return context.core.image.lightboxEnabled ? 'dialog' : '';
},
},
},
},
effects: {
core: {
image: {
initLightbox: async ( { context, ref } ) => {
if ( context.core.image.lightboxEnabled ) {
const focusableElements =
ref.querySelectorAll( focusableSelectors );
context.core.image.firstFocusableElement =
focusableElements[ 0 ];
context.core.image.lastFocusableElement =
focusableElements[ focusableElements.length - 1 ];

ref.querySelector( '.close-button' ).focus();
}
},
},
},
},
} );
113 changes: 113 additions & 0 deletions packages/block-library/src/image/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,116 @@
.wp-block-image figure {
margin: 0;
}

.wp-lightbox-container {

.img-container {
position: relative;
}

button {
border: none;
background: none;
cursor: zoom-in;
width: 100%;
height: 100%;
position: absolute;
z-index: 100;

&:focus-visible {
outline: 5px auto #212121;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: 5px;
}
}
}

.wp-lightbox-overlay {
position: fixed;
top: 0;
left: 0;
z-index: 100000;
overflow: hidden;
width: 100vw;
height: 100vh;
visibility: hidden;

.close-button {
font-size: 40px;
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
z-index: 5000000;
}

.wp-block-image {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: 3000000;
position: absolute;
flex-direction: column;
}

button {
border: none;
background: none;
}

.scrim {
width: 100%;
height: 100%;
position: absolute;
z-index: 2000000;
background-color: rgb(255, 255, 255);
opacity: 0.9;
}

&.initialized {
animation: both turn-off-visibility 300ms;

img {
animation: both turn-off-visibility 250ms;
}

&.active {
visibility: visible;
animation: both turn-on-visibility 250ms;

img {
animation: both turn-on-visibility 300ms;
}
}
}
}

@keyframes turn-on-visibility {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

@keyframes turn-off-visibility {
0% {
opacity: 1;
visibility: visible;
}
99% {
opacity: 0;
visibility: visible;
}
100% {
opacity: 0;
visibility: hidden;
}
}

html.has-lightbox-open {
overflow: hidden;
}
14 changes: 14 additions & 0 deletions packages/block-library/src/utils/interactivity/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
*/
import { useContext, useMemo, useEffect } from 'preact/hooks';
import { deepSignal, peek } from 'deepsignal';
/**
* Internal dependencies
*/
import { createPortal } from './portals.js';

/**
* Internal dependencies
Expand Down Expand Up @@ -53,6 +57,16 @@ export default () => {
{ priority: 5 }
);

// data-wp-body
directive( 'body', ( { props: { children }, context: inherited } ) => {
const { Provider } = inherited;
const inheritedValue = useContext( inherited );
return createPortal(
<Provider value={ inheritedValue }>{ children }</Provider>,
document.body
);
} );

// data-wp-effect.[name]
directive( 'effect', ( { directives: { effect }, context, evaluate } ) => {
const contextValue = useContext( context );
Expand Down
Loading

1 comment on commit ac13b94

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in ac13b94.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/5073745326
📝 Reported issues:

Please sign in to comment.