From 1aae4eee37ec80c6ea9b822fb43ecce73feb7df6 Mon Sep 17 00:00:00 2001 From: Sriram Krishnan Date: Sat, 6 Feb 2016 09:38:04 -0800 Subject: [PATCH] amp-accordion --- .../amp-accordion/0.1/amp-accordion.css | 67 ++++++++++++++ extensions/amp-accordion/0.1/amp-accordion.js | 76 ++++++++++++++++ .../0.1/test/test-amp-accordion.js | 90 +++++++++++++++++++ extensions/amp-accordion/amp-accordion.md | 64 +++++++++++++ gulpfile.js | 1 + src/base-element.js | 19 +++- src/render-delaying-extensions.js | 1 + test/manual/amp-accordion.amp.html | 36 ++++++++ tools/experiments/experiments.js | 8 ++ 9 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 extensions/amp-accordion/0.1/amp-accordion.css create mode 100644 extensions/amp-accordion/0.1/amp-accordion.js create mode 100644 extensions/amp-accordion/0.1/test/test-amp-accordion.js create mode 100644 extensions/amp-accordion/amp-accordion.md create mode 100644 test/manual/amp-accordion.amp.html diff --git a/extensions/amp-accordion/0.1/amp-accordion.css b/extensions/amp-accordion/0.1/amp-accordion.css new file mode 100644 index 000000000000..7602c7fd48a7 --- /dev/null +++ b/extensions/amp-accordion/0.1/amp-accordion.css @@ -0,0 +1,67 @@ +/** + * Copyright 2016 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. + */ + +/* Non-overridable properties */ +amp-accordion { + display: block !important; +} + +/* Make sections non-floatable */ +amp-accordion > section { + float: none !important; +} + +/* Hide all children and make them non-floatable */ +amp-accordion > section > *:nth-child(n) { + display: none !important; + float: none !important; +} + +/* Display the first 2 elements (heading and content) */ +amp-accordion > section > .-amp-accordion-header, +amp-accordion > section > .-amp-accordion-content { + display: block !important; + overflow: hidden !important; /* clearfix */ + position: relative !important; +} + +amp-accordion, +amp-accordion > section, +.-amp-accordion-header, +.-amp-accordion-content { + margin: 0; +} + + + +/* heading element*/ +.-amp-accordion-header { + cursor: pointer; + background-color: #efefef; + padding-right: 20px; + border: solid 1px #dfdfdf; +} + +/* Collapse content by default. */ +amp-accordion > section > .-amp-accordion-content { + display: none !important; +} + +/* Expand content when needed. */ +amp-accordion > section[expanded] > .-amp-accordion-content { + display: block !important; +} + diff --git a/extensions/amp-accordion/0.1/amp-accordion.js b/extensions/amp-accordion/0.1/amp-accordion.js new file mode 100644 index 000000000000..348a4ced7c82 --- /dev/null +++ b/extensions/amp-accordion/0.1/amp-accordion.js @@ -0,0 +1,76 @@ +/** + * Copyright 2016 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 {Layout} from '../../../src/layout'; +import {assert} from '../../../src/asserts'; +import {isExperimentOn} from '../../../src/experiments'; +import {log} from '../../../src/log'; + +/** @const */ +const EXPERIMENT = 'amp-accordion'; + +/** @const */ +const TAG = 'AmpAccordion'; + +class AmpAccordion extends AMP.BaseElement { + + /** @override */ + isLayoutSupported(layout) { + return layout == Layout.CONTAINER; + } + + /** @override */ + buildCallback() { + /** @const @private {!NodeList} */ + this.sections_ = this.getRealChildren(); + + /** @const @private {boolean} */ + this.isExperimentOn_ = isExperimentOn(this.getWin(), EXPERIMENT); + if (!this.isExperimentOn_) { + log.warn(TAG, `Experiment ${EXPERIMENT} disabled`); + return; + } + this.sections_.forEach(section => { + assert( + section.tagName.toLowerCase() == 'section', + 'Sections should be enclosed in a
tag, ' + + 'See https://github.com/ampproject/amphtml/blob/master/extensions/' + + 'amp-accordion/amp-accordion.md. Found in: %s', this.element); + const sectionComponents_ = section.children; + assert( + sectionComponents_.length == 2, + 'Each section must have exactly two children. ' + + 'See https://github.com/ampproject/amphtml/blob/master/extensions/' + + 'amp-accordion/amp-accordion.md. Found in: %s', this.element); + const header = sectionComponents_[0]; + const content = sectionComponents_[1]; + header.classList.add('-amp-accordion-header'); + content.classList.add('-amp-accordion-content'); + header.addEventListener('click', event => { + event.preventDefault(); + this.mutateElement(() => { + if (section.hasAttribute('expanded')) { + section.removeAttribute('expanded'); + } else { + section.setAttribute('expanded', ''); + } + }, content); + }); + }); + } +} + +AMP.registerElement('amp-accordion', AmpAccordion, $CSS$); diff --git a/extensions/amp-accordion/0.1/test/test-amp-accordion.js b/extensions/amp-accordion/0.1/test/test-amp-accordion.js new file mode 100644 index 000000000000..a9d693e3374d --- /dev/null +++ b/extensions/amp-accordion/0.1/test/test-amp-accordion.js @@ -0,0 +1,90 @@ +/** + * Copyright 2016 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 {Timer} from '../../../../src/timer'; +import {adopt} from '../../../../src/runtime'; +import {createIframePromise} from '../../../../testing/iframe'; +import {toggleExperiment} from '../../../../src/experiments'; +require('../../../../build/all/v0/amp-accordion-0.1.max'); + +adopt(window); + +describe('amp-accordion', () => { + const timer = new Timer(window); + function getAmpAccordion() { + return createIframePromise().then(iframe => { + toggleExperiment(iframe.win, 'amp-accordion', true); + const ampAccordion = iframe.doc.createElement('amp-accordion'); + for (let i = 0; i < 3; i++) { + const section = iframe.doc.createElement('section'); + section.innerHTML = "

Section " + i + "

Loreum ipsum
"; + ampAccordion.appendChild(section); + if (i == 1) { + section.setAttribute('expanded', ''); + } + } + return iframe.addElement(ampAccordion).then(() => { + return Promise.resolve({ + iframe: iframe, + ampAccordion: ampAccordion + }); + }); + }); + } + + it('should expand when header of a collapsed section is clicked', () => { + return getAmpAccordion().then(obj => { + const iframe = obj.iframe; + let clickEvent; + if (iframe.doc.createEvent) { + clickEvent = iframe.doc.createEvent('MouseEvent'); + clickEvent.initMouseEvent('click', true, true, iframe.win, 1); + } else { + clickEvent = iframe.doc.createEventObject(); + clickEvent.type = 'click'; + } + const headerElements = + iframe.doc.querySelectorAll('section > *:first-child'); + expect(headerElements[0].parentNode.hasAttribute('expanded')).to.be.false; + headerElements[0].dispatchEvent(clickEvent); + return timer.promise(50).then(() => { + expect(headerElements[0].parentNode.hasAttribute('expanded')) + .to.be.true; + }); + }); + }); + it('should collapse when header of an expanded section is clicked', () => { + return getAmpAccordion().then(obj => { + const iframe = obj.iframe; + let clickEvent; + if (iframe.doc.createEvent) { + clickEvent = iframe.doc.createEvent('MouseEvent'); + clickEvent.initMouseEvent('click', true, true, iframe.win, 1); + } else { + clickEvent = iframe.doc.createEventObject(); + clickEvent.type = 'click'; + } + const headerElements = + iframe.doc.querySelectorAll('section > *:first-child'); + expect(headerElements[1].parentNode.hasAttribute('expanded')).to.be.true; + headerElements[1].dispatchEvent(clickEvent); + return timer.promise(50).then(() => { + expect(headerElements[1].parentNode.hasAttribute('expanded')) + .to.be.false; + }); + }); + }); +}); diff --git a/extensions/amp-accordion/amp-accordion.md b/extensions/amp-accordion/amp-accordion.md new file mode 100644 index 000000000000..7aa51b9e7c6d --- /dev/null +++ b/extensions/amp-accordion/amp-accordion.md @@ -0,0 +1,64 @@ + + +### `amp-accordion` + +An accordion provides a way for viewers to have a glance at the outline of the content and jump to a section or their choice at their will. This would be extremely helpful for handheld mobile devices where even a couple of sentences in a section would lead to the viewer needing to scroll. + +#### Behavior + +Each of the `amp-accordion` component’s immediate children is considered a section in the accordion. Each of these nodes must be a `
` tag. + +- An `amp-accordion` can contain one or more `
`s as its direct children. +- Each `
` must contain only two direct children. +- The first child (of the section) will be considered as the heading of the section. Clicking/tapping on this section will trigger the expand/collapse behaviour. +- The second child (of the section) will be the content or the section +- There is no restriction on the type of tags that could be used for the `
`’s children. +- Any additional children of the `
` would be ignored not be displayed. (This should just be a safety backup and should be enforced in the validator) +- Clicking/tapping on the heading of a section expands/ or collapses the section. + +```html + +
+

Section 1

+

Bunch of awesome content

+
+
+

Section 2

+
Bunch of awesome content
+
+
+

Section 3

+ +
+
+``` + +#### Attributes + +**expanded** + +The `expanded` attribute can be set on any `
` that needs to be expanded on page load. + +#### Styling + +- You may use the `amp-accordion` element selector to style it freely. +- `amp-accordion` elements are always `display: block`. +- `
` and the heading and content element are not float-able. +- `
`s will have an `expanded` attribute when they are expanded. +- The content element is clear-fixed with `overflow: hidden` and hence cannot have scrollbars. +- margins of the `amp-accordion`, `
` and the heading and content elements are set to 0 and can be overridden in custom styles. +- Both the header and content elements are `position: relative`. \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 66e1681a525b..e766db42fadb 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -75,6 +75,7 @@ function buildExtensions(options) { // and update it if any of its required deps changed. // Each extension and version must be listed individually here. buildExtension('amp-access', '0.1', true, options); + buildExtension('amp-accordion', '0.1', true, options); buildExtension('amp-analytics', '0.1', false, options); buildExtension('amp-anim', '0.1', false, options); buildExtension('amp-audio', '0.1', false, options); diff --git a/src/base-element.js b/src/base-element.js index 35cea630a299..975ea230cc4a 100644 --- a/src/base-element.js +++ b/src/base-element.js @@ -493,7 +493,6 @@ export class BaseElement { * specified. Resource manager will perform the actual layout based on the * priority of this element and its children. * @param {!Element|!Array} elements - * @param {boolean} inLocalViewport * @protected */ scheduleLayout(elements) { @@ -554,6 +553,24 @@ export class BaseElement { this.resources_.attemptChangeHeight(this.element, newHeight, opt_callback); } + /** + * Runs the specified mutation on the element and ensures that measures + * and layouts performed for the affected elements. + * + * This method should be called whenever a significant mutations are done + * on the DOM that could affect layout of elements inside this subtree or + * its siblings. The top-most affected element should be specified as the + * first argument to this method and all the mutation work should be done + * in the mutator callback which is called in the "mutation" vsync phase. + * + * @param {function()} mutator + * @param {Element=} opt_element + * @return {!Promise} + */ + mutateElement(mutator, opt_element) { + this.resources_.mutateElement(opt_element || this.element, mutator); + } + /** * Schedules callback to be complete within the next batch. This call is * intended for heavy DOM mutations that typically cause re-layouts. diff --git a/src/render-delaying-extensions.js b/src/render-delaying-extensions.js index 9c7ad7a8374c..06ac834c24b9 100644 --- a/src/render-delaying-extensions.js +++ b/src/render-delaying-extensions.js @@ -25,6 +25,7 @@ import {timer} from './timer'; * @const {!Array} */ const EXTENSIONS = [ + 'amp-accordion', 'amp-dynamic-css-classes' ]; diff --git a/test/manual/amp-accordion.amp.html b/test/manual/amp-accordion.amp.html new file mode 100644 index 000000000000..c8fe9aedf861 --- /dev/null +++ b/test/manual/amp-accordion.amp.html @@ -0,0 +1,36 @@ + + + + + AMP #0 + + + + + + + + +

AMP #0

+ +
+

Section 1

+
+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus +
+
+
+

Section 2

+
+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus +
+
+
+

Section 3

+
+ +
+
+
+ + diff --git a/tools/experiments/experiments.js b/tools/experiments/experiments.js index 0633fee005c3..9532df0baec2 100644 --- a/tools/experiments/experiments.js +++ b/tools/experiments/experiments.js @@ -65,6 +65,14 @@ const EXPERIMENTS = [ spec: 'https://github.com/ampproject/amphtml/blob/master/extensions/' + 'amp-dynamic-css-classes/amp-dynamic-css-classes.md', }, + + // Amp Accordion + { + id: 'amp-accordion', + name: 'Amp Accordion', + spec: 'https://github.com/ampproject/amphtml/blob/master/extensions/' + + 'amp-accordion/amp-accordion.md', + }, ];