From: Kevin Ball Date: Sat, 20 May 2017 04:32:34 +0000 (-0700) Subject: Create explicit positioning method for Box and move dropdown positioning to be EXPLICIT X-Git-Tag: v6.4.0-rc1~19^2~23 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f224b0c8da35e50f06deba114dcfb28d4bea6e8a;p=thirdparty%2Ffoundation%2Ffoundation-sites.git Create explicit positioning method for Box and move dropdown positioning to be EXPLICIT --- diff --git a/js/foundation.dropdown.js b/js/foundation.dropdown.js index db63ba200..5b078b1e0 100644 --- a/js/foundation.dropdown.js +++ b/js/foundation.dropdown.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import { Keyboard } from './foundation.util.keyboard'; import { Box } from './foundation.util.box'; -import { GetYoDigits } from './foundation.util.core'; +import { GetYoDigits, rtl as Rtl } from './foundation.util.core'; import { Plugin } from './foundation.plugin'; // import "foundation.util.triggers.js"; @@ -18,6 +18,17 @@ import { Plugin } from './foundation.plugin'; * @requires foundation.util.triggers */ +const POSITIONS = ['left', 'right', 'top', 'bottom']; +const VERTICAL_ALIGNMENTS = ['top', 'bottom', 'center']; +const HORIZONTAL_ALIGNMENTS = ['left', 'right', 'center']; + +const ALIGNMENTS = { + 'left': VERTICAL_ALIGNMENTS, + 'right': VERTICAL_ALIGNMENTS, + 'top': HORIZONTAL_ALIGNMENTS, + 'bottom': HORIZONTAL_ALIGNMENTS +} + class Dropdown extends Plugin { /** * Creates a new instance of a dropdown. @@ -61,9 +72,9 @@ class Dropdown extends Plugin { }else{ this.$parent = null; } - this.options.positionClass = this.getPositionClass(); - this.counter = 4; - this.usedPositions = []; + this._setupPositionAndAlignment(); + + this.triedPositions = {}; this.$element.attr({ 'aria-hidden': 'true', 'data-yeti-box': $id, @@ -73,6 +84,18 @@ class Dropdown extends Plugin { this._events(); } + _setupPositionAndAlignment() { + if(this.options.position === 'left' || this.options.position === 'right') { + this.isHorizontallyPositioned = true; + } + if(this.options.position === 'top' || this.options.position === 'bottom') { + this.isVerticallyPositioned = true; + } + + this.position = this.options.position === 'auto' ? this._getDefaultPosition() : this.options.position; + this.alignment = this.options.alignment === 'auto' ? this._getDefaultAlignment() : this.options.alignment; + } + /** * Helper function to determine current orientation of dropdown pane. * @function @@ -88,86 +111,97 @@ class Dropdown extends Plugin { return position; } + _getDefaultPosition() { + // handle legacy classnames + var position = this.$element[0].className.match(/(top|left|right|bottom)/g); + if(position) { + return position[0]; + } else { + return 'bottom' + } + } + + _getDefaultAlignment() { + // handle legacy float appraoch + var horizontalPosition = /float-(\S+)/.exec(this.$anchor[0].className); + if(horizontalPosition) { + return horizontalPosition[1]; + } + + switch(this.position) { + case 'bottom': + case 'top': + return Rtl() ? 'left' : 'right'; + case 'left': + case 'right': + return 'bottom'; + } + } + /** - * Adjusts the dropdown panes orientation by adding/removing positioning classes. + * Adjusts the dropdown pane possible positions by iterating through alignments + * and positions. NOTE: Only used if position is auto, otherwise only alignments + * will be tried within the specified position. * @function * @private - * @param {String} position - position class to remove. */ - _reposition(position) { - this.usedPositions.push(position ? position : 'bottom'); - //default, try switching to opposite side - if(!position && (this.usedPositions.indexOf('top') < 0)){ - this.$element.addClass('top'); - }else if(position === 'top' && (this.usedPositions.indexOf('bottom') < 0)){ - this.$element.removeClass(position); - }else if(position === 'left' && (this.usedPositions.indexOf('right') < 0)){ - this.$element.removeClass(position) - .addClass('right'); - }else if(position === 'right' && (this.usedPositions.indexOf('left') < 0)){ - this.$element.removeClass(position) - .addClass('left'); - } + _reposition() { + } + - //if default change didn't work, try bottom or left first - else if(!position && (this.usedPositions.indexOf('top') > -1) && (this.usedPositions.indexOf('left') < 0)){ - this.$element.addClass('left'); - }else if(position === 'top' && (this.usedPositions.indexOf('bottom') > -1) && (this.usedPositions.indexOf('left') < 0)){ - this.$element.removeClass(position) - .addClass('left'); - }else if(position === 'left' && (this.usedPositions.indexOf('right') > -1) && (this.usedPositions.indexOf('bottom') < 0)){ - this.$element.removeClass(position); - }else if(position === 'right' && (this.usedPositions.indexOf('left') > -1) && (this.usedPositions.indexOf('bottom') < 0)){ - this.$element.removeClass(position); + /** + * Adjusts the dropdown pane possible positions by iterating through alignments + * on the current position. + * @function + * @private + */ + _realign() { + this._addTriedPosition(this.position, this.alignment) + var alignments = ALIGNMENTS[this.position] + var currentIdx = alignments.indexOf(this.alignment); + if(currentIdx === alignments.length - 1) { + this.alignment = alignments[0]; + } else { + this.alignment = alignments[currentIdx + 1]; } - //if nothing cleared, set to bottom - else{ - this.$element.removeClass(position); + } + + _addTriedPosition(position, alignment) { + this.triedPositions[position] = this.triedPositions[position] || [] + this.triedPositions[position].push(alignment); + } + + _positionsExhausted() { + if(this.options.position === 'auto') { + } else { + return this.triedPositions[this.position] && this.triedPositions[this.position].length == ALIGNMENTS[this.position].length; } - this.classChanged = true; - this.counter--; } /** - * Sets the position and orientation of the dropdown pane, checks for collisions. + * Sets the position and orientation of the dropdown pane, checks for collisions if allow-overlap is not true. * Recursively calls itself if a collision is detected, with a new position class. * @function * @private */ _setPosition() { if(this.$anchor.attr('aria-expanded') === 'false'){ return false; } - var position = this.getPositionClass(), - $eleDims = Box.GetDimensions(this.$element), - $anchorDims = Box.GetDimensions(this.$anchor), - _this = this, - direction = (position === 'left' ? 'left' : ((position === 'right') ? 'left' : 'top')), - param = (direction === 'top') ? 'height' : 'width', - offset = (param === 'height') ? this.options.vOffset : this.options.hOffset; - - if(($eleDims.width >= $eleDims.windowDims.width) || (!this.counter && !Box.ImNotTouchingYou(this.$element, this.$parent))){ - var newWidth = $eleDims.windowDims.width, - parentHOffset = 0; - if(this.$parent){ - var $parentDims = Box.GetDimensions(this.$parent), - parentHOffset = $parentDims.offset.left; - if ($parentDims.width < newWidth){ - newWidth = $parentDims.width; - } - } + var $eleDims = Box.GetDimensions(this.$element), + $anchorDims = Box.GetDimensions(this.$anchor); - this.$element.offset(Box.GetOffsets(this.$element, this.$anchor, 'center bottom', this.options.vOffset, this.options.hOffset + parentHOffset, true)).css({ - 'width': newWidth - (this.options.hOffset * 2), - 'height': 'auto' - }); - this.classChanged = true; - return false; - } - this.$element.offset(Box.GetOffsets(this.$element, this.$anchor, position, this.options.vOffset, this.options.hOffset)); + this.$element.offset(Box.GetExplicitOffsets(this.$element, this.$anchor, this.position, this.alignment, this.options.vOffset, this.options.hOffset)); - while(!Box.ImNotTouchingYou(this.$element, this.$parent, true) && this.counter){ - this._reposition(position); - this._setPosition(); + if(!this.options.allowOverlap) { + while(!Box.ImNotTouchingYou(this.$element, this.$parent, this.isVerticallyPositioned, this.isHorizontallyPositioned) && !this._positionsExhausted()){ + if(this.options.position === 'auto') { + this._reposition(); + } else { + console.log('realigning'); + this._realign(); + } + this._setPosition(); + } } } @@ -393,16 +427,16 @@ Dropdown.defaults = { * Number of pixels between the dropdown pane and the triggering element on open. * @option * @type {number} - * @default 1 + * @default 0 */ - vOffset: 1, + vOffset: 0, /** * Number of pixels between the dropdown pane and the triggering element on open. * @option * @type {number} - * @default 1 + * @default 0 */ - hOffset: 1, + hOffset: 0, /** * Class applied to adjust open position. JS will test and fill this in. * @option @@ -410,6 +444,28 @@ Dropdown.defaults = { * @default '' */ positionClass: '', + + /** + * Position of dropdown. Can be left, right, bottom, top, or auto. + * @option + * @type {string} + * @default 'auto' + */ + position: 'auto', + /** + * Alignment of dropdown relative to anchor. Can be left, right, bottom, top, center, or auto. + * @option + * @type {string} + * @default 'auto' + */ + alignment: 'auto', + /** + * Allow overlap of container/window. If false, dropdown will first try to position as defined by data-position and data-alignment, but reposition if it would cause an overflow. + * @option + * @type {boolean} + * @default false + */ + allowOverlap: false, /** * Allow the plugin to trap focus to the dropdown pane if opened with keyboard commands. * @option diff --git a/js/foundation.util.box.js b/js/foundation.util.box.js index ca13f8c9c..a5c6d082f 100644 --- a/js/foundation.util.box.js +++ b/js/foundation.util.box.js @@ -6,7 +6,8 @@ import { rtl as Rtl } from "./foundation.util.core"; var Box = { ImNotTouchingYou: ImNotTouchingYou, GetDimensions: GetDimensions, - GetOffsets: GetOffsets + GetOffsets: GetOffsets, + GetExplicitOffsets: GetExplicitOffsets } /** @@ -99,7 +100,9 @@ function GetDimensions(elem, test){ /** * Returns an object of top and left integer pixel values for dynamically rendered elements, - * such as: Tooltip, Reveal, and Dropdown + * such as: Tooltip, Reveal, and Dropdown. Maintained for backwards compatibility, and where + * you don't know alignment, but generally from + * 6.4 forward you should use GetExplicitOffsets, as GetOffsets conflates position and alignment. * @function * @param {jQuery} element - jQuery object for the element being positioned. * @param {jQuery} anchor - jQuery object for the element's anchor point. @@ -110,58 +113,34 @@ function GetDimensions(elem, test){ * TODO alter/rewrite to work with `em` values as well/instead of pixels */ function GetOffsets(element, anchor, position, vOffset, hOffset, isOverflow) { - var $eleDims = GetDimensions(element), - $anchorDims = anchor ? GetDimensions(anchor) : null; - switch (position) { case 'top': - return { - left: (Rtl() ? $anchorDims.offset.left - $eleDims.width + $anchorDims.width - hOffset: $anchorDims.offset.left + hOffset), - top: $anchorDims.offset.top - ($eleDims.height + vOffset) - } - break; - case 'left': - return { - left: $anchorDims.offset.left - ($eleDims.width + hOffset), - top: $anchorDims.offset.top + vOffset - } - break; - case 'right': - return { - left: $anchorDims.offset.left + $anchorDims.width + hOffset, - top: $anchorDims.offset.top + vOffset - } - break; + return Rtl() ? + GetExplicitOffsets(element, anchor, 'top', 'left', vOffset, hOffset, isOverflow) : + GetExplicitOffsets(element, anchor, 'top', 'right', vOffset, hOffset, isOverflow); + case 'bottom': + return Rtl() ? + GetExplicitOffsets(element, anchor, 'bottom', 'left', vOffset, hOffset, isOverflow) : + GetExplicitOffsets(element, anchor, 'bottom', 'right', vOffset, hOffset, isOverflow); case 'center top': - return { - left: ($anchorDims.offset.left + ($anchorDims.width / 2)) - ($eleDims.width / 2) + hOffset, - top: $anchorDims.offset.top - ($eleDims.height + vOffset) - } - break; + return GetExplicitOffsets(element, anchor, 'top', 'center', vOffset, hOffset, isOverflow); case 'center bottom': - return { - left: isOverflow ? hOffset : (($anchorDims.offset.left + ($anchorDims.width / 2)) - ($eleDims.width / 2)) + hOffset, - top: $anchorDims.offset.top + $anchorDims.height + vOffset - } - break; + return GetExplicitOffsets(element, anchor, 'bottom', 'center', vOffset, hOffset, isOverflow); case 'center left': - return { - left: $anchorDims.offset.left - ($eleDims.width + hOffset), - top: ($anchorDims.offset.top + vOffset + ($anchorDims.height / 2)) - ($eleDims.height / 2) - } - break; + return GetExplicitOffsets(element, anchor, 'left', 'center', vOffset, hOffset, isOverflow); case 'center right': - return { - left: $anchorDims.offset.left + $anchorDims.width + hOffset + 1, - top: ($anchorDims.offset.top + ($anchorDims.height / 2)) - ($eleDims.height / 2) + vOffset - } - break; + return GetExplicitOffsets(element, anchor, 'right', 'center', vOffset, hOffset, isOverflow); + case 'left bottom': + return GetExplicitOffsets(element, anchor, 'bottom', 'left', vOffset, hOffset, isOverflow); + case 'right bottom': + return GetExplicitOffsets(element, anchor, 'bottom', 'right', vOffset, hOffset, isOverflow); + // Backwards compatibility... this along with the reveal and reveal full + // classes are the only ones that didn't reference anchor case 'center': return { left: ($eleDims.windowDims.offset.left + ($eleDims.windowDims.width / 2)) - ($eleDims.width / 2) + hOffset, top: ($eleDims.windowDims.offset.top + ($eleDims.windowDims.height / 2)) - ($eleDims.height / 2 + vOffset) } - break; case 'reveal': return { left: ($eleDims.windowDims.width - $eleDims.width) / 2 + hOffset, @@ -173,24 +152,72 @@ function GetOffsets(element, anchor, position, vOffset, hOffset, isOverflow) { top: $eleDims.windowDims.offset.top } break; - case 'left bottom': - return { - left: $anchorDims.offset.left - hOffset, - top: $anchorDims.offset.top + $anchorDims.height + vOffset - }; - break; - case 'right bottom': - return { - left: $anchorDims.offset.left + $anchorDims.width + hOffset - $eleDims.width, - top: $anchorDims.offset.top + $anchorDims.height + vOffset - }; - break; default: return { left: (Rtl() ? $anchorDims.offset.left - $eleDims.width + $anchorDims.width - hOffset: $anchorDims.offset.left + hOffset), top: $anchorDims.offset.top + $anchorDims.height + vOffset } + + } + +} + +function GetExplicitOffsets(element, anchor, position, alignment, vOffset, hOffset, isOverflow) { + var $eleDims = GetDimensions(element), + $anchorDims = anchor ? GetDimensions(anchor) : null; + + var topVal, leftVal; + + // set position related attribute + + switch (position) { + case 'top': + topVal = $anchorDims.offset.top - ($eleDims.height + vOffset); + break; + case 'bottom': + topVal = $anchorDims.offset.top + $anchorDims.height + vOffset; + break; + case 'left': + leftVal = $anchorDims.offset.left - ($eleDims.width + hOffset); + break; + case 'right': + leftVal = $anchorDims.offset.left + $anchorDims.width + hOffset; + break; + } + + + // set alignment related attribute + switch (position) { + case 'top': + case 'bottom': + switch (alignment) { + case 'left': + leftVal = $anchorDims.offset.left + hOffset; + break; + case 'right': + leftVal = $anchorDims.offset.left - $eleDims.width + $anchorDims.width - hOffset; + break; + case 'center': + leftVal = isOverflow ? hOffset : (($anchorDims.offset.left + ($anchorDims.width / 2)) - ($eleDims.width / 2)) + hOffset; + break; + } + break; + case 'right': + case 'left': + switch (alignment) { + case 'bottom': + topVal = $anchorDims.offset.top - vOffset + $anchorDims.height - $eleDims.height; + break; + case 'top': + topVal = $anchorDims.offset.top + vOffset + break; + case 'center': + topVal = ($anchorDims.offset.top + vOffset + ($anchorDims.height / 2)) - ($eleDims.height / 2) + break; + } + break; } + return {top: topVal, left: leftVal}; } export {Box}; diff --git a/test/visual/dropdown/explicit-positioning.html b/test/visual/dropdown/explicit-positioning.html new file mode 100644 index 000000000..935ca2422 --- /dev/null +++ b/test/visual/dropdown/explicit-positioning.html @@ -0,0 +1,127 @@ + + + + + + + Foundation for Sites Testing + + + +
+

Dropdown: Explicit Positioning Content - no offsets

+ +

These dropdowns test various positioning and alignments. Valid + positions are left/right/top/bottom. Valid alignments are + left/right/top/bottom/center. Left align means left sides should line up. + Right align means right sides should line up. Center align means centers should line up. +

+ +

Top and Bottom positioned

+
+
+

Bottom Left

+ + +
+ +
+

Bottom Center

+ + +
+ +
+

Bottom Right

+ + +
+ +
+

Top Left

+ + +
+ +
+

Top Center

+ + +
+ +
+

Top Right

+ + +
+
+ + +

Left and Right Positioned

+
+
+

Right Top

+ + +
+
+

Left Top

+ + +
+ +
+

Right Center

+ + +
+
+

Left Center

+ + +
+ +
+

Right Bottom

+ + +
+
+

Left Bottom

+ + +
+
+
+ + + + + + diff --git a/test/visual/dropdown/offsets.html b/test/visual/dropdown/offsets.html index 5f847019d..25ec2a8e0 100644 --- a/test/visual/dropdown/offsets.html +++ b/test/visual/dropdown/offsets.html @@ -9,34 +9,114 @@
-

Dropdown: Positioning Content

+

Dropdown: Explicit Positioning Content - with offsets

-

These dropdowns test various positioning and position offsets

+

These dropdowns test various positioning and alignments WITH OFFSETS. + Valid positions are left/right/top/bottom. Valid alignments are + left/right/top/bottom/center. Left align means left sides should line up. + Right align means right sides should line up. Center align means centers should line up. + Positive Offsets should always be applied in a direction to create + space between the anchor and the dropdown. +

-

This dropdown should be offset by 10 down and 30 to the right

- - -

This dropdown has position left and alignment center, and should go off the screen left because it has allow-overlap true

- - -

This dropdown has position left and should go left if there is room and otherwise below

- - -

This dropdown has position bottom and alignment left should align with its top right corner at the bottom right of the button

- -