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

Add spacer element for non-sticky situations #7

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
54 changes: 51 additions & 3 deletions src/stickybits.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,17 @@ class Stickybits {
- offset = number
- stickyStart = number
- stickyStop = number
- spacer = dom element | null
Copy link
Owner

@yowainwright yowainwright Apr 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be controlled by the plugin.

  • It should go right into position: fixed code (not optional).
  • It should provide a css class stickybits-item-stub, or something like that.
  • The inline css of the stub should have a height set to the calculated stickybit item height.
  • The stub should be set to display:block; vs display:none when the stickybit item is set to position: fixed (but I know it is a bit more nuanced than that 😄).

See Fixed Sticky for reference.

Copy link
Author

@ykdr8 ykdr8 Apr 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yowainwright Thank you for comments 👍 Let me discuss before fixing.

  • It should go right into position: fixed code (not optional).

I agree that in terms of minimizing responsibility of instance. However I feel a bit uncertain that stickybits has to get stub element reference from current DOM tree again when destroying instance.
fixed-sticky does so , and it can lose the reference when re-getting. ( I know it's very rare case, but possible because DOM tree should be regarded as a global variable)

  • It should provide a css class stickybits-item-stub , or something like that.

OK, I'll add it after the other discussion!

  • The inline css of the stub should have a height set to the calculated stickybit item height.
  • The stub should be set to display:block; vs display:none when the stickybit item is set to position: fixed (but I know it is a bit more nuanced than that 😄).

They seem to be implemented around here, could you tell me if it is different from you intended ?

spacerStyles: {
...(pv === 'fixed' ? {
height: `${spacerHeight}px`,
display: spacerDisplay,
} : {
// Omit spacer if position is not 'fixed',
// even if position of state stuck is 'absolute'
display: 'none',
}),
},

Note: spacerDisplay is assumed it can be block or something else, such as table-header-group .

Copy link
Owner

@yowainwright yowainwright Apr 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @ykdr2017, I was just writing out instructions. 😃

As this issue supports a very small (but important use-case), I want to ensure that the footprint of this update is small.

- spacerHeight = number
- spacerDisplay = 'string'
- returns an instance object
*/
addInstance (el, props) {
const item = {
el,
parent: el.parentNode,
props,
spacer: null,
}
if (props.positionVal === 'fixed' || props.useStickyClasses) {
this.isWin = this.props.scrollEl === window
Expand All @@ -181,6 +185,15 @@ class Stickybits {
item.stateChange = 'default'
item.stateContainer = () => this.manageState(item)
se.addEventListener('scroll', item.stateContainer)
if (!props.noStyles) {
item.spacer = item.el.cloneNode()
// Avoid duplicate selectors
item.spacer.removeAttribute('id')
item.spacer.removeAttribute('class')
item.parent.insertBefore(item.spacer, item.el)
item.spacer.style.visibility = 'hidden'
item.spacer.style.display = 'none';
}
}
return item
}
Expand Down Expand Up @@ -228,8 +241,11 @@ class Stickybits {
computeScrollOffsets for Stickybits
- defines
- offset
- start
- stop
- stickyStart
- stickyStop
- stickyChange
- spacerHeight
- spacerDisplay
*/
computeScrollOffsets (item) {
const it = item
Expand All @@ -252,6 +268,8 @@ class Stickybits {
it.stickyStop = isTop
? parentBottom - (el.offsetHeight + it.offset)
: parentBottom - window.innerHeight
it.spacerHeight = el.offsetHeight
it.spacerDisplay = window.getComputedStyle(el).display
}

/*
Expand Down Expand Up @@ -287,6 +305,8 @@ class Stickybits {
const start = it.stickyStart
const change = it.stickyChange
const stop = it.stickyStop
const spacerHeight = it.spacerHeight
const spacerDisplay = it.spacerDisplay
// cache props
const pv = p.positionVal
const se = p.scrollEl
Expand Down Expand Up @@ -358,12 +378,25 @@ class Stickybits {
bottom: '',
[vp]: `${p.stickyBitStickyOffset}px`,
},
spacerStyles: {
...(pv === 'fixed' ? {
height: `${spacerHeight}px`,
display: spacerDisplay,
} : {
// Omit spacer if position is not 'fixed',
// even if position of state stuck is 'absolute'
display: 'none',
}),
},
classes: { [sticky]: true },
},
default: {
styles: {
[vp]: '',
},
spacerStyles: {
display: 'none',
},
classes: {},
},
stuck: {
Expand All @@ -379,6 +412,10 @@ class Stickybits {
bottom: '0',
} : {}),
},
spacerStyles: {
height: `${spacerHeight}px`,
display: spacerDisplay,
},
classes: { [stuck]: true },
},
}
Expand All @@ -403,12 +440,13 @@ class Stickybits {
---
- apply the given styles and classes to the element
*/
applyStyle ({ styles, classes }, item) {
applyStyle ({ styles, classes, spacerStyles }, item) {
// cache object
const it = item
const e = it.el
const p = it.props
const stl = e.style
const sp = it.spacer
// cache props
const ns = p.noStyles

Expand Down Expand Up @@ -438,6 +476,12 @@ class Stickybits {
for (const key in styles) {
stl[key] = styles[key]
}
if (sp) {
// eslint-disable-next-line no-unused-vars
for (const key in spacerStyles) {
sp.style[key] = spacerStyles[key]
}
}
}

update (updatedProps = null) {
Expand Down Expand Up @@ -472,6 +516,10 @@ class Stickybits {
)

this.toggleClasses(e.parentNode, p.parentClass)

if (instance.spacer) {
instance.parent.removeChild(instance.spacer)
}
}

/*
Expand Down
73 changes: 73 additions & 0 deletions tests/unit/test.stickybits.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,79 @@ test("stickybits doesn't change position style if noStyles is true", () => {
expect(parent.style['position']).toBe(positionStyle);
})

test("stickybits creates spacer if the element is fixed", () => {
document.body.innerHTML = '<div id="parent"></div>'
const stickybit = stickybits('#parent', { useFixed: true });

const item = stickybit.instances[0];
item.spacer.classList.add('spacer');

expect(item.spacer).not.toBe(null);
expect(document.querySelectorAll('.spacer').length).toBe(1);
})

test("stickybits doesn't display spacer if the element is fixed and state becomes `default`", () => {
document.body.innerHTML = '<div id="parent"></div>'
const stickybit = stickybits('#parent', { useFixed: true });

const item = stickybit.instances[0];

expect(item.state).toBe('default');
expect(item.spacer.style['display']).toBe('none');
})

test("stickybits displays spacer if the element is fixed and state becomes `sticky`", () => {
document.body.innerHTML = '<div id="parent"></div>'
const stickybit = stickybits('#parent', { useFixed: true });

const item = stickybit.instances[0];
stickybit.isWin = false;
item.props.scrollEl = { scrollTop: 100 };
item.stickyStart = 0;
item.stickyStop = 200;
stickybit.manageState(item)

expect(item.state).toBe('sticky');
expect(item.spacer.style['display']).toBe('block');
})

test("stickybits displays spacer if the element is fixed and state becomes `stuck`", () => {
document.body.innerHTML = '<div id="parent"></div>'
const stickybit = stickybits('#parent', { useFixed: true });

const item = stickybit.instances[0];
stickybit.isWin = false;
item.props.scrollEl = { scrollTop: 200 };
item.state = 'sticky';
item.stickyStart = 0;
item.stickyStop = 200;
stickybit.manageState(item)

expect(item.state).toBe('stuck');
expect(item.spacer.style['display']).toBe('block');
})

test("stickybits removes spacer when the instance is removed", () => {
document.body.innerHTML = '<div id="parent"></div>'
const stickybit = stickybits('#parent', { useFixed: true });

const item = stickybit.instances[0];
item.spacer.classList.add('spacer');
stickybit.removeInstance(item);

expect(document.querySelectorAll('.spacer').length).toBe(0);
})

test("stickybits doesn't create spacer if the element isn't fixed", () => {
document.body.innerHTML = '<div id="parent"></div>'
const stickybit = stickybits('#parent');

const item = stickybit.instances[0];

expect(item.spacer).toBe(null);
expect(document.querySelectorAll('div').length).toBe(1);
})

test('stickybits .addInstance interface', () => {
document.body.innerHTML = '<div id="manage-sticky"></div>'
const e = document.getElementById('manage-sticky')
Expand Down