Skip to content

Commit

Permalink
✨ Bento Lightbox Gallery (#32008)
Browse files Browse the repository at this point in the history
* Prototype

* Update with deeply nested example

* Preact skeleton

* cloneElement takes a children param

* Adopt new file structure

* Only use WithLightbox

* Make WithLightbox more customizable

* Lightbox content should support scrolling

* Support lightboxing BaseCarousel

* Fix types

* Specify cross-component file dependencies

* Dedupe contentRef and lightboxRef

* With lightbox does not need a ref

* Fix a11y attributes

* Don't render elements until the LightboxGallery opens

* Don't take thumbnailSrc for lightbox

* Split up files

* LightboxGallery -> LightboxGalleryProvider

* Update dependency list

* LightboxGalleryProvider render prop

* Remove key

* Rename autoLightbox as enableActivation

* Update type definitions

* Always render <Comp> layer

* Export directly

* lightbox.js moved to component.js
  • Loading branch information
caroqliu committed Feb 22, 2021
1 parent 839bf12 commit a324a18
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 3 deletions.
4 changes: 4 additions & 0 deletions build-system/compile/sources.js
Expand Up @@ -110,6 +110,10 @@ const CLOSURE_SRC_GLOBS = [
'extensions/amp-animation/**/*.js',
// Needed for amp-carousel 0.2, amp-inline-gallery, amp-stream-gallery
'extensions/amp-base-carousel/**/*.js',
// Needed for carousel autolightbox
'extensions/amp-lightbox-gallery/1.0/*.js',
// Needed for amp-lightbox-gallery using amp-lightbox
'extensions/amp-lightbox/1.0/*.js',
// For amp-bind in the web worker (ww.js).
'extensions/amp-bind/**/*.js',
// Needed to access to Variant interface from other extensions
Expand Down
5 changes: 5 additions & 0 deletions build-system/test-configs/dep-check-config.js
Expand Up @@ -246,6 +246,11 @@ exports.rules = [
'extensions/amp-stream-gallery/1.0/amp-stream-gallery.js->extensions/amp-base-carousel/1.0/base-carousel.jss.js',
'extensions/amp-stream-gallery/1.0/stream-gallery.js->extensions/amp-base-carousel/1.0/base-carousel.js',

// Autolightboxing dependencies
'extensions/amp-base-carousel/1.0/base-carousel.js->extensions/amp-lightbox-gallery/1.0/component.js',
'extensions/amp-base-carousel/1.0/scroller.js->extensions/amp-lightbox-gallery/1.0/context.js',
'extensions/amp-lightbox-gallery/1.0/provider.js->extensions/amp-lightbox/1.0/component.js',

// Facebook components
'extensions/amp-facebook-page/0.1/amp-facebook-page.js->extensions/amp-facebook/0.1/facebook-loader.js',
'extensions/amp-facebook-comments/0.1/amp-facebook-comments.js->extensions/amp-facebook/0.1/facebook-loader.js',
Expand Down
10 changes: 9 additions & 1 deletion extensions/amp-base-carousel/1.0/base-carousel.js
Expand Up @@ -27,6 +27,7 @@ import {CarouselContext} from './carousel-context';
import {ContainWrapper} from '../../../src/preact/component';
import {Scroller} from './scroller';
import {WithAmpContext} from '../../../src/preact/context';
import {WithLightbox} from '../../amp-lightbox-gallery/1.0/component';
import {forwardRef} from '../../../src/preact/compat';
import {isRTL} from '../../../src/dom';
import {mod} from '../../../src/utils/math';
Expand Down Expand Up @@ -93,6 +94,7 @@ function BaseCarouselWithRef(
controls = Controls.AUTO,
defaultSlide = 0,
dir = Direction.AUTO,
lightbox = false,
loop,
mixedLength = false,
onFocus,
Expand Down Expand Up @@ -298,7 +300,6 @@ function BaseCarouselWithRef(
direction: rtl ? Direction.RTL : Direction.LTR,
}}
ref={containRef}
contentRef={contentRef}
onFocus={(e) => {
if (onFocus) {
onFocus(e);
Expand All @@ -319,6 +320,12 @@ function BaseCarouselWithRef(
}}
tabIndex="0"
wrapperClassName={classes.carousel}
contentAs={lightbox ? WithLightbox : 'div'}
contentRef={contentRef}
contentProps={{
enableActivation: false,
render: () => children,
}}
{...rest}
>
{!hideControls && (
Expand All @@ -336,6 +343,7 @@ function BaseCarouselWithRef(
alignment={snapAlign}
autoAdvanceCount={autoAdvanceCount}
axis={axis}
lightbox={lightbox}
loop={loop}
mixedLength={mixedLength}
restingIndex={currentSlide}
Expand Down
12 changes: 12 additions & 0 deletions extensions/amp-base-carousel/1.0/scroller.js
Expand Up @@ -21,13 +21,15 @@ import {
getPercentageOffsetFromAlignment,
scrollContainerToElement,
} from './dimensions';
import {LightboxGalleryContext} from '../../amp-lightbox-gallery/1.0/context';
import {debounce} from '../../../src/utils/rate-limit';
import {forwardRef} from '../../../src/preact/compat';
import {mod} from '../../../src/utils/math';
import {setStyle} from '../../../src/style';
import {toWin} from '../../../src/types';
import {
useCallback,
useContext,
useImperativeHandle,
useLayoutEffect,
useMemo,
Expand Down Expand Up @@ -58,6 +60,7 @@ function ScrollerWithRef(
autoAdvanceCount,
axis,
children,
lightbox,
loop,
mixedLength,
restingIndex,
Expand Down Expand Up @@ -136,13 +139,15 @@ function ScrollerWithRef(
*/
const scrollOffset = useRef(0);

const {open: openLightbox} = useContext(LightboxGalleryContext);
const slides = renderSlides(
{
alignment,
children,
loop,
mixedLength,
offsetRef,
openLightbox: lightbox && openLightbox,
pivotIndex,
restingIndex,
snap,
Expand Down Expand Up @@ -348,6 +353,7 @@ function renderSlides(
mixedLength,
restingIndex,
offsetRef,
openLightbox,
pivotIndex,
snap,
snapBy,
Expand All @@ -357,6 +363,11 @@ function renderSlides(
classes
) {
const {length} = children;
const lightboxProps = openLightbox && {
role: 'button',
tabindex: '0',
onClick: () => openLightbox(),
};
const slides = children.map((child, index) => {
const key = `slide-${child.key || index}`;
return (
Expand All @@ -376,6 +387,7 @@ function renderSlides(
style={{
flex: mixedLength ? '0 0 auto' : `0 0 ${100 / visibleCount}%`,
}}
{...lightboxProps}
>
{child}
</div>
Expand Down
18 changes: 18 additions & 0 deletions extensions/amp-lightbox-gallery/1.0/component.js
@@ -0,0 +1,18 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export {LightboxGalleryProvider} from './provider';
export {WithLightbox} from './consumer';
48 changes: 48 additions & 0 deletions extensions/amp-lightbox-gallery/1.0/component.type.js
@@ -0,0 +1,48 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/** @externs */

/** @const */
var LightboxGalleryDef = {};

/**
* @typedef {{
* children: (PreactDef.Renderable),
* render: (function():PreactDef.Renderable|undefined),
* }}
*/
LightboxGalleryDef.Props;

/**
* @typedef {{
* as: (string|undefined),
* children: (!PreactDef.Renderable),
* enableActivation: (boolean|undefined),
* onClick: (function(Event)|undefined)
* render: (function():PreactDef.Renderable),
* }}
*/
LightboxGalleryDef.WithLightboxProps;

/**
* @typedef {{
* deregister: (function(string):undefined),
* register: (function(string, Element):undefined),
* open: (function:undefined),
* }}
*/
LightboxGalleryDef.ContextProps;
73 changes: 73 additions & 0 deletions extensions/amp-lightbox-gallery/1.0/consumer.js
@@ -0,0 +1,73 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as Preact from '../../../src/preact';
import {LightboxGalleryContext} from './context';
import {sequentialIdGenerator} from '../../../src/utils/id-generator';
import {
useContext,
useLayoutEffect,
useMemo,
useState,
} from '../../../src/preact';

const generateLightboxItemKey = sequentialIdGenerator();

const DEFAULT_ARIA_LABEL = 'Open content in a lightbox view.';
const DEFAULT_ACTIVATION_PROPS = {
'aria-label': DEFAULT_ARIA_LABEL,
role: 'button',
tabIndex: '0',
};

/**
* @param {!LightboxGalleryDef.WithLightboxProps} props
* @return {PreactDef.Renderable}
*/
export function WithLightbox({
as: Comp = 'div',
children,
enableActivation = true,
onClick: customOnClick,
render = () => children,
...rest
}) {
const [genKey] = useState(generateLightboxItemKey);
const {open, register, deregister} = useContext(LightboxGalleryContext);
useLayoutEffect(() => {
register(genKey, render);
return () => deregister(genKey);
}, [genKey, deregister, register, render]);

const activationProps = useMemo(
() =>
enableActivation && {
...DEFAULT_ACTIVATION_PROPS,
onClick: () => {
if (customOnClick) {
customOnClick();
}
open();
},
},
[customOnClick, enableActivation, open]
);
return (
<Comp {...activationProps} {...rest}>
{children}
</Comp>
);
}
25 changes: 25 additions & 0 deletions extensions/amp-lightbox-gallery/1.0/context.js
@@ -0,0 +1,25 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {createContext} from '../../../src/preact';

const LightboxGalleryContext = createContext(
/** @type {LightboxGalleryDef.ContextProps} */ ({
deregister: () => {},
register: () => {},
open: () => {},
})
);
export {LightboxGalleryContext};
72 changes: 72 additions & 0 deletions extensions/amp-lightbox-gallery/1.0/provider.js
@@ -0,0 +1,72 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Preact from '../../../src/preact';
import {Lightbox} from './../../amp-lightbox/1.0/component';
import {LightboxGalleryContext} from './context';
import {useCallback, useRef} from '../../../src/preact';

/**
* @param {!LightboxGalleryDef.Props} props
* @return {PreactDef.Renderable}
*/
export function LightboxGalleryProvider({children, render}) {
const lightboxRef = useRef(null);
const renderers = useRef([]);
const lightboxElements = useRef([]);
const register = (key, render) => {
renderers.current[key] = render;
};
const deregister = (key) => {
delete lightboxElements.current[key];
delete renderers.current[key];
};
const context = {
deregister,
register,
open: () => lightboxRef.current.open(),
};

const renderElements = useCallback(() => {
renderers.current.forEach((render, index) => {
if (!lightboxElements.current[index]) {
lightboxElements.current[index] = render();
}
});
}, []);
return (
<>
<Lightbox
onBeforeOpen={() => renderElements()}
ref={lightboxRef}
scrollable
>
{/* TODO: This needs an actual close button UI */}
<div
aria-label="Close the lightbox"
role="button"
tabIndex="0"
onClick={() => lightboxRef.current.close()}
>
Close lightbox
</div>
<div>{lightboxElements.current}</div>
</Lightbox>
<LightboxGalleryContext.Provider value={context}>
{render ? render() : children}
</LightboxGalleryContext.Provider>
</>
);
}

0 comments on commit a324a18

Please sign in to comment.