Skip to content

Commit cc1e9f2

Browse files
committed
move offset logic into scroll-into-view
1 parent d7e00f3 commit cc1e9f2

12 files changed

+265
-354
lines changed

addon-test-support/scroll-into-view-mock.js

+35-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@ export default function mockScrollIntoView() {
55
let mockScrollIntoViewFunction = function () {
66
elementsInvokedOn.push(this);
77
};
8+
9+
let mockScrollToElementWithOffsetFunction = function (options) {
10+
elementsInvokedOn.push(options);
11+
};
12+
813
// manually mocking native function
9-
let preExistingScrollFunction = window.Element.prototype.scrollIntoView;
14+
let preExistingScrollIntoViewFunction =
15+
window.Element.prototype.scrollIntoView;
16+
let preExistingScrollToFunction = window.scrollTo;
1017
window.Element.prototype.scrollIntoView = mockScrollIntoViewFunction;
18+
window.scrollTo = mockScrollToElementWithOffsetFunction;
19+
1120
// helper fuctions that will be returned
12-
let scrollIntoViewCalledWith = (selector) => {
21+
let scrollIntoViewCalledWith = (selector, options = {}) => {
1322
let element;
1423
// check if it's a string and get the object
1524
if (typeof selector === 'string') {
@@ -18,11 +27,33 @@ export default function mockScrollIntoView() {
1827
// element was passed in
1928
element = selector;
2029
}
21-
return elementsInvokedOn.includes(element);
30+
31+
if (!options.offset) {
32+
return elementsInvokedOn.includes(element);
33+
}
34+
35+
if (!element || !document) {
36+
return;
37+
}
38+
const { behavior = 'smooth', offset = 0, left = 0 } = options;
39+
40+
const elementTop =
41+
element.getBoundingClientRect().top -
42+
document.body.getBoundingClientRect().top -
43+
offset;
44+
45+
return elementsInvokedOn.some((calledOptions) => {
46+
return (
47+
behavior === calledOptions.behavior &&
48+
elementTop === calledOptions.top &&
49+
left === calledOptions.left
50+
);
51+
});
2252
};
2353

2454
let resetMock = () => {
25-
window.Element.prototype.scrollIntoView = preExistingScrollFunction;
55+
window.Element.prototype.scrollIntoView = preExistingScrollIntoViewFunction;
56+
window.scrollTo = preExistingScrollToFunction;
2657
};
2758

2859
return {

addon-test-support/scroll-to-element-with-offset-mock.js

-46
This file was deleted.

addon/modifiers/scroll-into-view.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,20 @@ export default modifier(function scrollIntoView(
1212

1313
shouldScrollPromise.then((shouldScrollValue) => {
1414
if (shouldScrollValue && element && !hasBeenRemoved) {
15-
element.scrollIntoView(options);
15+
if (!options?.offset) {
16+
element.scrollIntoView(options);
17+
} else {
18+
const { behavior = 'auto', offset = 0, left = 0 } = options;
19+
20+
window?.scrollTo({
21+
behavior,
22+
top:
23+
element.getBoundingClientRect().top -
24+
document.body.getBoundingClientRect().top -
25+
offset,
26+
left,
27+
});
28+
}
1629
}
1730
});
1831

addon/modifiers/scroll-to-element-with-offset.js

-31
This file was deleted.

app/modifiers/scroll-to-element-with-offset.js

-1
This file was deleted.

docs/modifiers/scroll-to-element-with-offset.js docs/modifiers/scroll-into-view.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@ import { tracked } from '@glimmer/tracking';
33
import { action } from '@ember/object';
44

55
export default class EsButtonComponent extends Component {
6+
@tracked shouldScrollWithOffset;
67
@tracked shouldScroll;
78
@tracked offset = 25;
89

910
@action
10-
onScrollToElementWithOffset() {
11+
onScrollIntoView() {
1112
this.shouldScroll = true;
1213
}
1314

15+
@action
16+
onScrollIntoViewWithOffset() {
17+
this.shouldScrollWithOffset = true;
18+
}
19+
1420
@action
1521
onOffsetChange(event) {
1622
// clear the shouldScroll value to prevent scrolling on offset change
17-
this.shouldScroll = false;
23+
this.shouldScrollWithOffset = false;
1824
this.offset = event.target.value;
1925
}
2026
}

docs/modifiers/scroll-into-view.md

+36-4
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,41 @@
11
# scroll-into-view
22

3-
This modifier calls `scrollIntoView` on the modified element.
3+
This modifier scrolls to the associated element. By default it uses `scrollIntoView`, but if an offset is passed as an option it uses `scrollTo` and calculates the `options.top` attribute.
44

55

66
## When you should use this modifier
77

8-
You should use this modifier whenever you need to have an element scrolled into view on element insert.
8+
You should use this modifier whenever you need to have an element scrolled into view. If there is a element, such as a fixed header, passing in an `offset` will scroll to the element minus the `offset` value.
9+
910

1011

1112
## Basic Usage
1213

1314
`scroll-into-view` expects the named `shouldScroll` parameter and an optional `options` named parameter. See [scrollIntoView](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) for the list of possible values and properties of `options`.
1415

15-
```handlebars{data-execute=false}
16-
<div {{scroll-into-view shouldScroll=this.shouldScrollPromise options=true}}></div>
16+
17+
```handlebars
18+
<div {{scroll-into-view shouldScroll=this.shouldScroll options=(hash behavior="smooth")}}>
19+
<button type="button" {{on "click" this.onScrollIntoView}}>
20+
Trigger scroll-into-view on click
21+
</button>
22+
</div>
23+
```
24+
25+
`shouldScroll` can be either a Boolean or a Promise that resolves to a truthy or falsy value. It does not handle a rejected Promise.
26+
27+
28+
### Usage with offset
29+
30+
When passing in an offset, it will call `scrollTo`. See [scrollTo](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo) for the list of possible values and properties of `options`.
31+
32+
```handlebars
33+
<div {{scroll-into-view shouldScroll=this.shouldScrollWithOffset options=(hash offset=this.offset behavior="smooth")}}>
34+
<input type="number" value={{this.offset}} {{on "change" this.onOffsetChange}}>
35+
<button type="button" {{on "click" this.onScrollIntoViewWithOffset}}>
36+
Trigger scroll-into-view with offset on click
37+
</button>
38+
</div>
1739
```
1840

1941
`shouldScroll` can be either a Boolean or a Promise that resolves to a truthy or falsy value. It does not handle a rejected Promise.
@@ -45,9 +67,19 @@ function('test scroll into view', (assert) => {
4567
...
4668
assert.ok(this.mockHelperFunctions.scrollIntoViewCalledWith('[data-test-scroll-into-view-selector]'), 'element scrolled into view');
4769
});
70+
71+
function('test scroll into view with offset', (assert) => {
72+
...
73+
await render(
74+
hbs`<div {{scroll-into-view shouldScroll=true options=(hash offset=25)}} data-test-scroll-into-view-selector></div>`
75+
);
76+
...
77+
assert.ok(this.mockHelperFunctions.scrollIntoViewCalledWith('[data-test-scroll-into-view-selector]', { behavior: 'smooth', top: 25, left: 0 }), 'scrolled to element');
78+
});
4879
```
4980

5081

5182
## Browser Support
5283

5384
This feature is [supported](https://caniuse.com/?search=scrollIntoView) in the latest versions of every browser.
85+
This feature is [supported](https://caniuse.com/?search=scrollTo) in the latest versions of every browser.

docs/modifiers/scroll-to-element-with-offset.md

-58
This file was deleted.

tests/integration/modifiers/scroll-into-view-mock-test.js

+41
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,46 @@ module(
4646
)
4747
);
4848
});
49+
50+
test('scroll into view returns correct parameters with using offset', async function (assert) {
51+
this.options = {
52+
offset: 10,
53+
behavior: 'auto',
54+
left: 10,
55+
};
56+
57+
await render(
58+
hbs`<div style="height: 10px" {{scroll-into-view shouldScroll=true options=this.options}} data-test-scroll-into-view-selector></div>
59+
<div style="height: 10px" data-test-scroll-into-view-not-modified></div>
60+
<div style="height: 10px" {{scroll-into-view shouldScroll=false options=this.options}} data-test-scroll-into-view-not-scrolled></div>`
61+
);
62+
63+
assert.ok(
64+
this.mockHelperFunctions.scrollIntoViewCalledWith(
65+
'[data-test-scroll-into-view-selector]',
66+
this.options
67+
),
68+
'scrollIntoViewCalledWith should return true for the given element selector'
69+
);
70+
assert.ok(
71+
this.mockHelperFunctions.scrollIntoViewCalledWith(
72+
find('[data-test-scroll-into-view-selector]'),
73+
this.options
74+
),
75+
'scrollIntoViewCalledWith should return true for the given element'
76+
);
77+
assert.notOk(
78+
this.mockHelperFunctions.scrollIntoViewCalledWith(
79+
'[data-test-scroll-into-view-not-modified]'
80+
),
81+
"scrollIntoViewCalledWith should return false if the element doesn't have the modifier"
82+
);
83+
assert.notOk(
84+
this.mockHelperFunctions.scrollIntoViewCalledWith(
85+
'[data-test-scroll-into-view-not-scrolled]'
86+
),
87+
'scrollIntoViewCalledWith should return false if the modifier was not triggered'
88+
);
89+
});
4990
}
5091
);

0 commit comments

Comments
 (0)