Skip to content

Commit e0bba8e

Browse files
committed
Change to use topOffset and leftOffset
- this allows for symmetric use for vertical and horizontal offsets
1 parent cc1e9f2 commit e0bba8e

File tree

6 files changed

+85
-54
lines changed

6 files changed

+85
-54
lines changed

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

+15-6
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,34 @@ export default function mockScrollIntoView() {
2828
element = selector;
2929
}
3030

31-
if (!options.offset) {
31+
if (options?.topOffset === undefined && options?.leftOffset === undefined) {
3232
return elementsInvokedOn.includes(element);
3333
}
3434

3535
if (!element || !document) {
3636
return;
3737
}
38-
const { behavior = 'smooth', offset = 0, left = 0 } = options;
38+
const { behavior = 'smooth', leftOffset, topOffset } = options;
39+
40+
const elementLeft =
41+
leftOffset === undefined
42+
? 0
43+
: element.getBoundingClientRect().left -
44+
document.body.getBoundingClientRect().left -
45+
leftOffset;
3946

4047
const elementTop =
41-
element.getBoundingClientRect().top -
42-
document.body.getBoundingClientRect().top -
43-
offset;
48+
topOffset === undefined
49+
? 0
50+
: element.getBoundingClientRect().top -
51+
document.body.getBoundingClientRect().top -
52+
topOffset;
4453

4554
return elementsInvokedOn.some((calledOptions) => {
4655
return (
4756
behavior === calledOptions.behavior &&
4857
elementTop === calledOptions.top &&
49-
left === calledOptions.left
58+
elementLeft === calledOptions.left
5059
);
5160
});
5261
};

addon/modifiers/scroll-into-view.js

+20-6
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,31 @@ export default modifier(function scrollIntoView(
1212

1313
shouldScrollPromise.then((shouldScrollValue) => {
1414
if (shouldScrollValue && element && !hasBeenRemoved) {
15-
if (!options?.offset) {
15+
if (
16+
options?.topOffset === undefined &&
17+
options?.leftOffset === undefined
18+
) {
1619
element.scrollIntoView(options);
1720
} else {
18-
const { behavior = 'auto', offset = 0, left = 0 } = options;
21+
const { behavior = 'auto', topOffset, leftOffset } = options;
22+
23+
const left =
24+
leftOffset === undefined
25+
? 0
26+
: element.getBoundingClientRect().left -
27+
document.body.getBoundingClientRect().left -
28+
leftOffset;
29+
30+
const top =
31+
topOffset === undefined
32+
? 0
33+
: element.getBoundingClientRect().top -
34+
document.body.getBoundingClientRect().top -
35+
topOffset;
1936

2037
window?.scrollTo({
2138
behavior,
22-
top:
23-
element.getBoundingClientRect().top -
24-
document.body.getBoundingClientRect().top -
25-
offset,
39+
top,
2640
left,
2741
});
2842
}

docs/modifiers/scroll-into-view.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { action } from '@ember/object';
55
export default class EsButtonComponent extends Component {
66
@tracked shouldScrollWithOffset;
77
@tracked shouldScroll;
8-
@tracked offset = 25;
8+
@tracked topOffset = 25;
9+
@tracked leftOffset = 25;
910

1011
@action
1112
onScrollIntoView() {
@@ -18,9 +19,16 @@ export default class EsButtonComponent extends Component {
1819
}
1920

2021
@action
21-
onOffsetChange(event) {
22+
onTopOffsetChange(event) {
2223
// clear the shouldScroll value to prevent scrolling on offset change
2324
this.shouldScrollWithOffset = false;
24-
this.offset = event.target.value;
25+
this.topOffset = event.target.value;
26+
}
27+
28+
@action
29+
onLeftOffsetChange(event) {
30+
// clear the shouldScroll value to prevent scrolling on offset change
31+
this.shouldScrollWithOffset = false;
32+
this.leftOffset = event.target.value;
2533
}
2634
}

docs/modifiers/scroll-into-view.md

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# scroll-into-view
22

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.
3+
This modifier scrolls to the associated element. By default it uses `scrollIntoView`, but if a top or left offset is passed as an option it uses `scrollTo` and calculates the `options.top` and/or `options.left` 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. If there is a element, such as a fixed header, passing in an `offset` will scroll to the element minus the `offset` value.
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 or sidebar, passing in a `topOffset` or `leftOffset` will scroll to the element minus the that offset value.
99

1010

1111

@@ -27,11 +27,19 @@ You should use this modifier whenever you need to have an element scrolled into
2727

2828
### Usage with offset
2929

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`.
30+
When passing in an offset, it will call [scrollTo](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo), and the `options` parameter is designed to correspond to its `options`. The `options.behavior` operates the same, however, instead of `top` and `left` there are `topOffset` and `leftOffset`, respectively. If an offset value is not set then the value passed to `scrollTo` is 0, e.g. `options = { topOffset: 10 }` results in `element.scrollTo({ top: [computedValue], left: 0 })`. Experiment with the below example, you may need to zoom and resize the window to see a horizontal scrollbar.
31+
3132

3233
```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}}>
34+
<div {{scroll-into-view shouldScroll=this.shouldScrollWithOffset options=(hash topOffset=this.topOffset leftOffset=this.leftOffset behavior="smooth")}}>
35+
<div>
36+
<label for="topOffset">Top Offset: </label>
37+
<input name="topOffset" type="number" value={{this.topOffset}} {{on "change" this.onTopOffsetChange}}>
38+
</div>
39+
<div>
40+
<label for="leftOffset">Left Offset: </label>
41+
<input name="leftOffset" type="number" value={{this.leftOffset}} {{on "change" this.onLeftOffsetChange}}>
42+
</div>
3543
<button type="button" {{on "click" this.onScrollIntoViewWithOffset}}>
3644
Trigger scroll-into-view with offset on click
3745
</button>

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ module(
4949

5050
test('scroll into view returns correct parameters with using offset', async function (assert) {
5151
this.options = {
52-
offset: 10,
5352
behavior: 'auto',
54-
left: 10,
53+
leftOffset: 10,
54+
topOffset: 20,
5555
};
5656

5757
await render(

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

+24-32
Original file line numberDiff line numberDiff line change
@@ -98,25 +98,20 @@ module('Integration | Modifier | scroll-into-view', function (hooks) {
9898
);
9999
});
100100

101-
module('with offset', function (_hooks) {
102-
_hooks.beforeEach(function () {
103-
this.scrollToSpy = sandbox.spy(window, 'scrollTo');
104-
this.getBoundingClientRectStub = sandbox.stub(
101+
module('with offsets', function (offsetHooks) {
102+
offsetHooks.beforeEach(function () {
103+
this.scrollToSpy = sinon.spy(window, 'scrollTo');
104+
this.getBoundingClientRectStub = sinon.stub(
105105
Element.prototype,
106106
'getBoundingClientRect'
107107
);
108-
this.getBoundingClientRectStub.onCall(0).returns({ top: 100 });
109-
this.getBoundingClientRectStub.onCall(1).returns({ top: 25 });
110-
});
111108

112-
_hooks.afterEach(function () {
113-
this.scrollToSpy = null;
114-
sandbox.restore();
109+
this.getBoundingClientRectStub.returns({ left: 100, top: 100 });
115110
});
116111

117-
test('it renders and passes default options to scrollTo', async function (assert) {
112+
test('it renders and passes default `behavior` to scrollTo', async function (assert) {
118113
this.options = {
119-
offset: 50,
114+
topOffset: 50,
120115
};
121116

122117
await render(
@@ -128,19 +123,12 @@ module('Integration | Modifier | scroll-into-view', function (hooks) {
128123
'auto',
129124
'scrollTo was called with correct params'
130125
);
131-
132-
assert.strictEqual(
133-
this.scrollToSpy.args[0][0].left,
134-
0,
135-
'scrollTo was called with correct params'
136-
);
137126
});
138127

139-
test('it renders and passes behavior and left options to scrollTo', async function (assert) {
128+
test('it renders and passes behavior to scrollTo', async function (assert) {
140129
this.options = {
141-
offset: 50,
142130
behavior: 'smooth',
143-
left: 10,
131+
topOffset: 50,
144132
};
145133

146134
await render(
@@ -152,19 +140,20 @@ module('Integration | Modifier | scroll-into-view', function (hooks) {
152140
'smooth',
153141
'scrollTo was called with correct params'
154142
);
155-
156-
assert.strictEqual(
157-
this.scrollToSpy.args[0][0].left,
158-
10,
159-
'scrollTo was called with correct params'
160-
);
161143
});
162144

163145
test('it renders and calculates correct top offset for scrollTo when offset is passed in', async function (assert) {
164146
this.options = {
165-
offset: 50,
147+
topOffset: 50,
148+
leftOffset: 40,
166149
};
167150

151+
this.getBoundingClientRectStub.onCall(0).returns({ left: 100 });
152+
this.getBoundingClientRectStub.onCall(1).returns({ left: 25 });
153+
154+
this.getBoundingClientRectStub.onCall(2).returns({ top: 100 });
155+
this.getBoundingClientRectStub.onCall(3).returns({ top: 25 });
156+
168157
await render(
169158
hbs`<div id="test" {{scroll-into-view shouldScroll=true options=this.options}}></div>`
170159
);
@@ -173,7 +162,7 @@ module('Integration | Modifier | scroll-into-view', function (hooks) {
173162
this.scrollToSpy.args[0][0],
174163
{
175164
behavior: 'auto',
176-
left: 0,
165+
left: 35,
177166
top: 25,
178167
},
179168
'scrollTo was called with correct params'
@@ -182,7 +171,8 @@ module('Integration | Modifier | scroll-into-view', function (hooks) {
182171

183172
test('it does not call scrollTo when shouldScroll is false', async function (assert) {
184173
this.options = {
185-
offset: 50,
174+
topOffset: 50,
175+
leftOffset: 40,
186176
};
187177

188178
await render(
@@ -194,7 +184,8 @@ module('Integration | Modifier | scroll-into-view', function (hooks) {
194184

195185
test('it renders when shouldScroll resolves to true', async function (assert) {
196186
this.options = {
197-
offset: 50,
187+
topOffset: 50,
188+
leftOffset: 40,
198189
};
199190

200191
let resolvePromise;
@@ -213,7 +204,8 @@ module('Integration | Modifier | scroll-into-view', function (hooks) {
213204

214205
test('it does not render when shouldScroll resolves to false', async function (assert) {
215206
this.options = {
216-
offset: 50,
207+
topOffset: 50,
208+
leftOffset: 40,
217209
};
218210

219211
let resolvePromise;

0 commit comments

Comments
 (0)