Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resize cols to prevent bottom scrollbar when right scrollbar appears #883

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 80 additions & 39 deletions addon/-private/column-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,9 @@ export default EmberObject.extend({
return;
}

// cache right scrollbar width so we can detect if it appears or disappears
this._scrollbarWidth = this.getScrollbarWidth();

let leaves = get(this, 'root.leaves');

// ensures that min and max widths are respected _before_ `applyFillMode()`
Expand All @@ -692,7 +695,8 @@ export default EmberObject.extend({
let initialFillMode = get(this, 'initialFillMode');

if (isSlackModeEnabled && initialFillMode) {
this.applyFillMode(initialFillMode);
let containerWidth = this.getContainerWidth();
this.applyFillMode(initialFillMode, containerWidth);
}

this.ensureWidthConstraint();
Expand All @@ -707,13 +711,58 @@ export default EmberObject.extend({
return;
}

let isSlackModeEnabled = get(this, 'isSlackModeEnabled');
let fillMode = get(this, 'fillMode');

// accommodate scrollbar as it appears/vanishes
let scrollbarDelta = this.getScrollbarWidth() - this._scrollbarWidth;
if (scrollbarDelta !== 0) {
let treeWidth = get(this, 'root.width');
let contentWidth = get(this, 'root.contentWidth');
let containerWidth = this.getContainerWidth();
let extra = containerWidth - contentWidth;

// If `scrollbarDelta` is positive, it means the right scrollbar just
// appeared; in this case, we shrink the columns if they are within a
// scrollbar width of the container. This prevents a bottom scrollbar
// from appearing when it is not necessary.

// If `scrollbarDelta` is negative, it means the right scrollbar just
// vanished; in this case, we enlarge if the rightmost column's edge is
// between one and two scrollbar widths of the container's edge. This
// ensures the inverse action to the above.

if (
(scrollbarDelta > 0 && (0 < -extra && -extra <= scrollbarDelta)) ||
(scrollbarDelta < 0 && (-scrollbarDelta <= extra && extra < -2 * scrollbarDelta))
) {
let targetWidth = treeWidth - scrollbarDelta;
this.applyFillMode(fillMode, targetWidth);
}
}

this._scrollbarWidth += scrollbarDelta;

// if `widthConstraint` is set to a slack variety, fill excess space
// with a slack column before applying `fillMode`
let isSlackModeEnabled = get(this, 'isSlackModeEnabled');
if (isSlackModeEnabled) {
this.updateSlackColumn();
}

this.applyFillMode();
// only trigger fill mode if `widthConstraint` has been violated
let widthConstraint = get(this, 'widthConstraint');
let contentWidth = get(this, 'root.contentWidth');
let containerWidth = this.getContainerWidth();
let delta = containerWidth - contentWidth;
if (
(widthConstraint === WIDTH_CONSTRAINT.EQ_CONTAINER && delta !== 0) ||
(widthConstraint === WIDTH_CONSTRAINT.EQ_CONTAINER_SLACK && delta !== 0) ||
(widthConstraint === WIDTH_CONSTRAINT.LTE_CONTAINER && delta < 0) ||
(widthConstraint === WIDTH_CONSTRAINT.GTE_CONTAINER && delta > 0) ||
(widthConstraint === WIDTH_CONSTRAINT.GTE_CONTAINER_SLACK && delta > 0)
) {
this.applyFillMode(fillMode, containerWidth);
}
},

/**
Expand All @@ -736,46 +785,34 @@ export default EmberObject.extend({
},

/**
Attempts to satisfy tree's width constraint by resizing columns according
to the specifid `fillMode`. If no `fillMode` is specified, the tree's
own `fillMode` property will be used.
Attempts to fit columns to container size by resizing columns using the
specified `fillMode` to fit in the specified target width.

@param {String} fillMode
@param {Number} targetWidth
*/
applyFillMode(fillMode) {
fillMode = fillMode || get(this, 'fillMode');

let widthConstraint = get(this, 'widthConstraint');
let containerWidth = this.getContainerWidth();
applyFillMode(fillMode, targetWidth) {
let contentWidth = get(this, 'root.contentWidth');
let delta = containerWidth - contentWidth;

if (
(widthConstraint === WIDTH_CONSTRAINT.EQ_CONTAINER && delta !== 0) ||
(widthConstraint === WIDTH_CONSTRAINT.EQ_CONTAINER_SLACK && delta !== 0) ||
(widthConstraint === WIDTH_CONSTRAINT.LTE_CONTAINER && delta < 0) ||
(widthConstraint === WIDTH_CONSTRAINT.GTE_CONTAINER && delta > 0) ||
(widthConstraint === WIDTH_CONSTRAINT.GTE_CONTAINER_SLACK && delta > 0)
) {
if (fillMode === FILL_MODE.EQUAL_COLUMN) {
set(this, 'root.width', containerWidth);
} else if (fillMode === FILL_MODE.FIRST_COLUMN) {
this.resizeColumn(0, delta);
} else if (fillMode === FILL_MODE.LAST_COLUMN) {
let isSlackModeEnabled = get(this, 'isSlackModeEnabled');
let columns = get(this, 'root.subcolumnNodes');
let lastColumnIndex = isSlackModeEnabled ? columns.length - 2 : columns.length - 1;
this.resizeColumn(lastColumnIndex, delta);
} else if (fillMode === FILL_MODE.NTH_COLUMN) {
let fillColumnIndex = get(this, 'fillColumnIndex');

assert(
"fillMode 'nth-column' must have a fillColumnIndex defined",
!isEmpty(fillColumnIndex)
);

this.resizeColumn(fillColumnIndex, delta);
}
let delta = targetWidth - contentWidth;

if (fillMode === FILL_MODE.EQUAL_COLUMN) {
set(this, 'root.width', targetWidth);
} else if (fillMode === FILL_MODE.FIRST_COLUMN) {
this.resizeColumn(0, delta);
} else if (fillMode === FILL_MODE.LAST_COLUMN) {
let isSlackModeEnabled = get(this, 'isSlackModeEnabled');
let columns = get(this, 'root.subcolumnNodes');
let lastColumnIndex = isSlackModeEnabled ? columns.length - 2 : columns.length - 1;
this.resizeColumn(lastColumnIndex, delta);
} else if (fillMode === FILL_MODE.NTH_COLUMN) {
let fillColumnIndex = get(this, 'fillColumnIndex');

assert(
"fillMode 'nth-column' must have a fillColumnIndex defined",
!isEmpty(fillColumnIndex)
);

this.resizeColumn(fillColumnIndex, delta);
}
},

Expand All @@ -797,6 +834,10 @@ export default EmberObject.extend({
return getInnerClientRect(this.container).width * this.scale + containerWidthAdjustment;
},

getScrollbarWidth() {
return this.container.offsetWidth - this.container.clientWidth;
},

getReorderBounds(node) {
let parent = get(node, 'parent');
let { scale } = this;
Expand Down
11 changes: 7 additions & 4 deletions addon/-private/utils/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ export function getInnerClientRect(element) {
let borderLeftWidth = parseFloat(computedStyle.getPropertyValue('border-left-width')) / scale;
let borderRightWidth = parseFloat(computedStyle.getPropertyValue('border-right-width')) / scale;

let scrollbarBottomHeight = element.offsetHeight - element.clientHeight;
let scrollbarRightWidth = element.offsetWidth - element.clientWidth;

return {
top: outerClientRect.top + borderTopWidth,
bottom: outerClientRect.bottom - borderBottomWidth,
bottom: outerClientRect.bottom - borderBottomWidth - scrollbarBottomHeight,
left: outerClientRect.left + borderLeftWidth,
right: outerClientRect.right - borderRightWidth,
right: outerClientRect.right - borderRightWidth - scrollbarRightWidth,

height: outerClientRect.height - borderTopWidth - borderBottomWidth,
width: outerClientRect.width - borderLeftWidth - borderRightWidth,
height: outerClientRect.height - borderTopWidth - borderBottomWidth - scrollbarBottomHeight,
width: outerClientRect.width - borderLeftWidth - borderRightWidth - scrollbarRightWidth,
};
}

Expand Down
13 changes: 9 additions & 4 deletions tests/integration/components/scroll-indicators-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,17 @@ module('Integration | scroll indicators', function() {
},
});

let firstHeader = table.headers.objectAt(0);

// not necessarily 100px wide because of integer division
let lastHeader = table.headers.objectAt(table.headers.length - 1);

assert.equal(
table.isScrollIndicatorRendered('right'),
true,
'right scroll indicator is initially shown'
);
assert.ok(isOffset('right', 100), 'right scroll indicator is offset');
assert.ok(isOffset('right', lastHeader.width), 'right scroll indicator is offset');
assert.equal(
table.isScrollIndicatorRendered('left'),
false,
Expand All @@ -154,13 +159,13 @@ module('Integration | scroll indicators', function() {
true,
'right scroll indicator is shown during partial scroll'
);
assert.ok(isOffset('right', 100), 'right scroll indicator is offset');
assert.ok(isOffset('right', lastHeader.width), 'right scroll indicator is offset');
assert.equal(
table.isScrollIndicatorRendered('left'),
true,
'left scroll indicator is shown during partial scroll'
);
assert.ok(isOffset('left', 100), 'left scroll indicator is offset');
assert.ok(isOffset('left', firstHeader.width), 'left scroll indicator is offset');

// scroll horizontally to the end
await scrollTo('[data-test-ember-table-overflow]', SCROLL_MAX, 0);
Expand All @@ -175,7 +180,7 @@ module('Integration | scroll indicators', function() {
true,
'left scroll indicator is still shown at end of scroll'
);
assert.ok(isOffset('left', 100), 'left scroll indicator is offset');
assert.ok(isOffset('left', firstHeader.width), 'left scroll indicator is offset');
});

test('top scroll indicator positioned below header', async function(assert) {
Expand Down