blob: a173f12769010782a2a3f05493b3e02f16b594d8 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* 'settings-section' shows a paper material themed section with a header
* which shows its page title.
*
* The section can expand vertically to fill its container's padding edge.
*
* Example:
*
* <settings-section page-title="[[pageTitle]]" section="privacy">
* <!-- Insert your section controls here -->
* </settings-section>
*/
let SettingsSectionElement = Polymer({
is: 'settings-section',
properties: {
/**
* The section name should match a name specified in route.js. The
* MainPageBehavior will expand this section if this section name matches
* currentRoute.section.
*/
section: String,
/**
* Title for the section header. Initialize so we can use the
* getTitleHiddenStatus_ method for accessibility.
*/
pageTitle: {
type: String,
value: '',
},
/**
* A CSS attribute used for temporarily hiding a SETTINGS-SECTION for the
* purposes of searching.
*/
hiddenBySearch: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
/**
* Original height of the collapsed section, used as the target height when
* collapsing after being expanded.
* TODO(michaelpg): Get the height dynamically when collapsing using the
* card's main page.
* @private
*/
collapsedHeight_: {
type: Number,
value: NaN,
},
},
/**
* Freezes the section's height so its card can be removed from the flow
* without affecting the layout of the surrounding sections.
* @param {boolean} frozen True to freeze, false to unfreeze.
* @private
*/
setFrozen: function(frozen) {
const card = this.$.card;
if (frozen) {
this.style.height = this.clientHeight + 'px';
const cardHeight = card.offsetHeight;
const cardWidth = card.offsetWidth;
// If the section is not displayed yet (e.g., navigated directly to a
// sub-page), cardHeight and cardWidth are 0, so do not set the height or
// width explicitly.
if (cardHeight && cardWidth) {
card.style.height = cardHeight + 'px';
card.style.width = cardWidth + 'px';
}
// Place the section's card at its current position but removed from the
// flow.
card.style.top = card.getBoundingClientRect().top + 'px';
this.classList.add('frozen');
} else {
// Restore the section to its normal height.
if (!this.classList.contains('frozen'))
return;
this.classList.remove('frozen');
this.$.card.style.top = '';
this.$.card.style.height = '';
this.$.card.style.width = '';
this.style.height = '';
}
},
/**
* Calling this method fires the 'settings-section-expanded event'.
*/
setExpanded_: function() {
this.classList.add('expanded');
this.fire('settings-section-expanded');
},
/**
* @return {boolean} True if the section is currently rendered and not
* already expanded or transitioning.
*/
canAnimateExpand: function() {
return !this.classList.contains('expanding') &&
!this.classList.contains('expanded') && this.$.card.clientHeight > 0;
},
immediateExpand: function(container) {
// Target position is the container's top edge in the viewport.
const containerTop = container.getBoundingClientRect().top;
this.$.card.position = 'fixed';
this.$.card.top = containerTop + 'px';
this.$.card.height = 'calc(100% - ' + containerTop + 'px)';
this.setExpanded_();
},
/**
* Animates the section expanding to fill the container. The section is fixed
* in the viewport during the animation, making it safe to adjust the rest of
* the DOM after calling this. The section adds the "expanding" class while
* the animation plays and "expanded" after it finishes.
*
* @param {!HTMLElement} container The scrolling container to fill.
* @return {!settings.animation.Animation}
*/
animateExpand: function(container) {
// Set the section's height so its card can be removed from the flow
// without affecting the surrounding sections during the animation.
this.collapsedHeight_ = this.clientHeight;
this.style.height = this.collapsedHeight_ + 'px';
this.classList.add('expanding');
// Start the card in place, at its distance from the container's padding.
const startTop = this.$.card.getBoundingClientRect().top + 'px';
const startHeight = this.$.card.clientHeight + 'px';
// Target position is the container's top edge in the viewport.
const containerTop = container.getBoundingClientRect().top;
const endTop = containerTop + 'px';
// The card should stretch from the bottom of the toolbar to the bottom of
// the page. calc(100% - top) lets the card resize if the window resizes.
const endHeight = 'calc(100% - ' + containerTop + 'px)';
const animation =
this.animateCard_('fixed', startTop, endTop, startHeight, endHeight);
// The empty onRejected function prevents the promise from skipping forward
// to the next then() with a rejection callback.
animation.finished.then(this.setExpanded_.bind(this), () => {}).then(() => {
// Unset these changes whether the animation finished or canceled.
this.classList.remove('expanding');
this.style.height = '';
});
return animation;
},
/**
* @return {boolean} True if the section is currently expanded and we know
* what the collapsed height should be.
*/
canAnimateCollapse: function() {
return this.classList.contains('expanded') && this.clientHeight > 0 &&
!Number.isNaN(this.collapsedHeight_);
},
/**
* Prepares for the animation before the other sections become visible.
* Call before animateCollapse().
* @param {!HTMLElement} container
*/
setUpAnimateCollapse: function(container) {
// Prepare the dimensions and set position: fixed.
this.$.card.style.width = this.$.card.clientWidth + 'px';
this.$.card.style.height = this.$.card.clientHeight + 'px';
this.$.card.style.top = container.getBoundingClientRect().top + 'px';
this.$.card.style.position = 'fixed';
// The section can now collapse back into its original height the page so
// the other sections appear in the right places.
this.classList.remove('expanded');
this.classList.add('collapsing');
this.style.height = this.collapsedHeight_ + 'px';
},
/**
* Collapses an expanded section's card back into position in the main page.
* Call after calling animateCollapse(), unhiding other content and scrolling.
* @param {!HTMLElement} container The scrolling container the card fills.
* @return {!settings.animation.Animation}
*/
animateCollapse: function(container) {
// Make the card position: absolute, so scrolling is less of a crapshoot.
// First find the current distance between this section and the card using
// fixed coordinates; the absolute distance will be the same.
const fixedCardTop = this.$.card.getBoundingClientRect().top;
const fixedSectionTop = this.getBoundingClientRect().top;
const distance = fixedCardTop - fixedSectionTop;
// The target position is right below our header.
const headerStyle = getComputedStyle(this.$.header);
const cardTargetTop = this.$.header.offsetHeight +
parseFloat(headerStyle.marginBottom) +
parseFloat(headerStyle.marginTop);
// Start the card at its current height and distance from our top.
const startTop = distance + 'px';
const startHeight = this.$.card.style.height;
// End at the bottom of our header.
const endTop = cardTargetTop + 'px';
const endHeight = (this.collapsedHeight_ - cardTargetTop) + 'px';
// The card no longer needs position: fixed.
this.$.card.style.position = '';
// Collapse this section, animate the card into place, and remove its
// other properties.
const animation =
this.animateCard_('absolute', startTop, endTop, startHeight, endHeight);
this.$.card.style.width = '';
this.$.card.style.height = '';
this.$.card.style.top = '';
animation.finished
.then(
() => {
this.classList.remove('expanded');
},
function() {})
.then(() => {
// The card now determines the section's height automatically.
this.style.height = '';
this.classList.remove('collapsing');
});
return animation;
},
/**
* Helper function to animate the card's position and height.
* @param {string} position CSS position property.
* @param {string} startTop Initial top value.
* @param {string} endTop Target top value.
* @param {string} startHeight Initial height value.
* @param {string} endHeight Target height value.
* @return {!settings.animation.Animation}
* @private
*/
animateCard_: function(position, startTop, endTop, startHeight, endHeight) {
// Width does not change.
const width = this.$.card.clientWidth + 'px';
const startFrame = {
position: position,
width: width,
top: startTop,
height: startHeight,
};
const endFrame = {
position: position,
width: width,
top: endTop,
height: endHeight,
};
const options = /** @type {!KeyframeEffectOptions} */ ({
duration: settings.animation.Timing.DURATION,
easing: settings.animation.Timing.EASING,
});
return new settings.animation.Animation(
this.$.card, [startFrame, endFrame], options);
},
/**
* Get the value to which to set the aria-hidden attribute of the section
* heading.
* @return {boolean|string} A return value of false will not add aria-hidden
* while aria-hidden requires a string of 'true' to be hidden as per aria
* specs. This function ensures we have the right return type.
* @private
*/
getTitleHiddenStatus_: function() {
return this.pageTitle ? false : 'true';
}
});