diff --git a/build-system/compile/sources.js b/build-system/compile/sources.js index 3f97cfc23426..158d83a488cd 100644 --- a/build-system/compile/sources.js +++ b/build-system/compile/sources.js @@ -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 diff --git a/build-system/test-configs/dep-check-config.js b/build-system/test-configs/dep-check-config.js index c2e4cc6accf4..22abaece03bc 100644 --- a/build-system/test-configs/dep-check-config.js +++ b/build-system/test-configs/dep-check-config.js @@ -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', diff --git a/extensions/amp-base-carousel/1.0/base-carousel.js b/extensions/amp-base-carousel/1.0/base-carousel.js index 9749d742628c..d54caf8ec31a 100644 --- a/extensions/amp-base-carousel/1.0/base-carousel.js +++ b/extensions/amp-base-carousel/1.0/base-carousel.js @@ -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'; @@ -93,6 +94,7 @@ function BaseCarouselWithRef( controls = Controls.AUTO, defaultSlide = 0, dir = Direction.AUTO, + lightbox = false, loop, mixedLength = false, onFocus, @@ -298,7 +300,6 @@ function BaseCarouselWithRef( direction: rtl ? Direction.RTL : Direction.LTR, }} ref={containRef} - contentRef={contentRef} onFocus={(e) => { if (onFocus) { onFocus(e); @@ -319,6 +320,12 @@ function BaseCarouselWithRef( }} tabIndex="0" wrapperClassName={classes.carousel} + contentAs={lightbox ? WithLightbox : 'div'} + contentRef={contentRef} + contentProps={{ + enableActivation: false, + render: () => children, + }} {...rest} > {!hideControls && ( @@ -336,6 +343,7 @@ function BaseCarouselWithRef( alignment={snapAlign} autoAdvanceCount={autoAdvanceCount} axis={axis} + lightbox={lightbox} loop={loop} mixedLength={mixedLength} restingIndex={currentSlide} diff --git a/extensions/amp-base-carousel/1.0/scroller.js b/extensions/amp-base-carousel/1.0/scroller.js index d07ef08853c1..34fa0e3edad0 100644 --- a/extensions/amp-base-carousel/1.0/scroller.js +++ b/extensions/amp-base-carousel/1.0/scroller.js @@ -21,6 +21,7 @@ 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'; @@ -28,6 +29,7 @@ import {setStyle} from '../../../src/style'; import {toWin} from '../../../src/types'; import { useCallback, + useContext, useImperativeHandle, useLayoutEffect, useMemo, @@ -58,6 +60,7 @@ function ScrollerWithRef( autoAdvanceCount, axis, children, + lightbox, loop, mixedLength, restingIndex, @@ -136,6 +139,7 @@ function ScrollerWithRef( */ const scrollOffset = useRef(0); + const {open: openLightbox} = useContext(LightboxGalleryContext); const slides = renderSlides( { alignment, @@ -143,6 +147,7 @@ function ScrollerWithRef( loop, mixedLength, offsetRef, + openLightbox: lightbox && openLightbox, pivotIndex, restingIndex, snap, @@ -348,6 +353,7 @@ function renderSlides( mixedLength, restingIndex, offsetRef, + openLightbox, pivotIndex, snap, snapBy, @@ -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 ( @@ -376,6 +387,7 @@ function renderSlides( style={{ flex: mixedLength ? '0 0 auto' : `0 0 ${100 / visibleCount}%`, }} + {...lightboxProps} > {child} diff --git a/extensions/amp-lightbox-gallery/1.0/component.js b/extensions/amp-lightbox-gallery/1.0/component.js new file mode 100644 index 000000000000..9c249a0d6acc --- /dev/null +++ b/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'; diff --git a/extensions/amp-lightbox-gallery/1.0/component.type.js b/extensions/amp-lightbox-gallery/1.0/component.type.js new file mode 100644 index 000000000000..b420a1402f6d --- /dev/null +++ b/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; diff --git a/extensions/amp-lightbox-gallery/1.0/consumer.js b/extensions/amp-lightbox-gallery/1.0/consumer.js new file mode 100644 index 000000000000..aff8a049715e --- /dev/null +++ b/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 ( + + {children} + + ); +} diff --git a/extensions/amp-lightbox-gallery/1.0/context.js b/extensions/amp-lightbox-gallery/1.0/context.js new file mode 100644 index 000000000000..deafc69a6488 --- /dev/null +++ b/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}; diff --git a/extensions/amp-lightbox-gallery/1.0/provider.js b/extensions/amp-lightbox-gallery/1.0/provider.js new file mode 100644 index 000000000000..d23506fd86da --- /dev/null +++ b/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 ( + <> + renderElements()} + ref={lightboxRef} + scrollable + > + {/* TODO: This needs an actual close button UI */} +
lightboxRef.current.close()} + > + Close lightbox +
+
{lightboxElements.current}
+
+ + {render ? render() : children} + + + ); +} diff --git a/extensions/amp-lightbox-gallery/1.0/storybook/Basic.js b/extensions/amp-lightbox-gallery/1.0/storybook/Basic.js new file mode 100644 index 000000000000..93118d396a29 --- /dev/null +++ b/extensions/amp-lightbox-gallery/1.0/storybook/Basic.js @@ -0,0 +1,112 @@ +/** + * 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 {BaseCarousel} from '../../../amp-base-carousel/1.0/base-carousel'; +import {LightboxGalleryProvider, WithLightbox} from '../component'; +import {withA11y} from '@storybook/addon-a11y'; +import {withKnobs} from '@storybook/addon-knobs'; + +export default { + title: 'LightboxGallery', + component: LightboxGalleryProvider, + decorators: [withKnobs, withA11y], +}; + +export const _default = () => { + return ( + <> + + + + + +

abc

+ ( + smaller img + )} + /> +

abc

+ + + +
+
+
+ + + +
+
+
+
+ + ); +}; + +export const carousel = () => { + return ( + <> + + + + + + + + + + + + + ); +}; diff --git a/src/preact/component/contain.js b/src/preact/component/contain.js index 332307d20182..d92dea51fbbf 100644 --- a/src/preact/component/contain.js +++ b/src/preact/component/contain.js @@ -58,8 +58,10 @@ function ContainWrapperWithRef( paint = false, wrapperClassName, wrapperStyle, + contentAs: ContentComp = 'div', contentRef, contentClassName, + contentProps, contentStyle, children, 'className': className, @@ -81,7 +83,8 @@ function ContainWrapperWithRef( contain: CONTAIN[containIndex], }} > -
{children} -
+ ); }