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

feat: add Stack effect #38

Merged
merged 9 commits into from Jul 27, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
126 changes: 102 additions & 24 deletions src/index.ts
Expand Up @@ -47,13 +47,19 @@ export interface SnackOptions {
*/
position?: Position
theme?: 'string' | ThemeRules
/**
* Maximum stacks to display, earlier created snackbar will be hidden
* @default 3
*/
maxStack?: number
}

export interface SnackInstanceOptions {
timeout: number
actions: Action[]
position: Position
theme: ThemeRules
maxStack: number
}

export interface SnackResult {
Expand All @@ -67,7 +73,17 @@ export interface ThemeRules {
actionColor?: string
}

let instances: Snackbar[] = []
let instances: { [k: string]: Snackbar[] } = {
left: [],
center: [],
right: []
}

let instanceStackStatus: { [k: string]: boolean } = {
left: true,
center: true,
right: true
}

const themes: { [name: string]: ThemeRules } = {
light: {
Expand All @@ -93,19 +109,22 @@ export class Snackbar {
timeout = 0,
actions = [{ text: 'dismiss', callback: () => this.destroy() }],
position = 'center',
theme = 'dark'
theme = 'dark',
maxStack = 3
} = options
this.message = message
this.options = {
timeout,
actions,
position,
maxStack,
theme: typeof theme === 'string' ? themes[theme] : theme
}

this.wrapper = this.getWrapper(this.options.position)
this.insert()
instances.push(this)
instances[this.options.position].push(this)
this.stack()
}

get theme() {
Expand All @@ -132,20 +151,24 @@ export class Snackbar {
el.setAttribute('aria-hidden', 'false')

const { backgroundColor, textColor, boxShadow, actionColor } = this.theme

const container = document.createElement('div')
container.className = 'snackbar--container'
if (backgroundColor) {
el.style.backgroundColor = backgroundColor
container.style.backgroundColor = backgroundColor
}
if (textColor) {
el.style.color = textColor
container.style.color = textColor
}
if (boxShadow) {
el.style.boxShadow = boxShadow
container.style.boxShadow = boxShadow
}
el.appendChild(container)

const text = document.createElement('div')
text.className = 'snackbar--text'
text.textContent = this.message
el.appendChild(text)
container.appendChild(text)

// Add action buttons
if (this.options.actions) {
Expand All @@ -170,44 +193,97 @@ export class Snackbar {
this.destroy()
}
})
el.appendChild(button)
container.appendChild(button)
}
}

this.startTimer()

// Stop timer when mouseenter
// Restart timer when mouseleave
el.addEventListener('mouseenter', () => {
this.stopTimer()
this.expand()
})
el.addEventListener('mouseleave', () => {
this.startTimer()
this.stack()
})

this.el = el

this.wrapper.appendChild(el)
}

stack() {
instanceStackStatus[this.options.position] = true
const positionInstances = instances[this.options.position]
const l = positionInstances.length - 1
positionInstances.forEach((instance, i) => {
// Resume all instances' timers if applicable
instance.startTimer()
if (instance.el) {
instance.el.style.transform = `translate3d(0, -${(l - i) * 15}px, -${l -
i}px) scale(${1 - 0.05 * (l - i)})`
if (l - i >= this.options.maxStack) {
instance.el.style.opacity = '0'
} else {
instance.el.style.opacity = '1'
}
}
})
}

expand() {
yaodingyd marked this conversation as resolved.
Show resolved Hide resolved
instanceStackStatus[this.options.position] = false
const positionInstances = instances[this.options.position]
const l = positionInstances.length - 1
positionInstances.forEach((instance, i) => {
// Stop all instances' timers to prevent destroy
instance.stopTimer()
if (instance.el) {
instance.el.style.transform = `translate3d(0, -${(l - i) *
instance.el.clientHeight}px, 0) scale(1)`
if (l - i >= this.options.maxStack) {
instance.el.style.opacity = '0'
} else {
instance.el.style.opacity = '1'
}
}
})
}

/**
* Destory the snackbar
*/
async destroy() {
const { el, wrapper } = this
if (el) {
this.el = undefined
// Transition the snack away.
// Animate the snack away.
el.setAttribute('aria-hidden', 'true')
await new Promise(resolve => {
const eventName = getTransitionEvent(el)
const eventName = getAnimationEvent(el)
if (eventName) {
el.addEventListener(eventName, () => resolve())
} else {
resolve()
}
})
wrapper.removeChild(el)
// Remove instance from the instances array
const positionInstances = instances[this.options.position]
let index: number | undefined = undefined
for (let i = 0; i < positionInstances.length; i++) {
if (positionInstances[i].el === el) {
index = i
break
}
}
if (index !== undefined) {
positionInstances.splice(index, 1)
}
// Based on current status, refresh stack or expand style
if (instanceStackStatus[this.options.position]) {
this.stack()
} else {
this.expand()
}
}
}

Expand All @@ -228,17 +304,17 @@ export class Snackbar {
}
}

function getTransitionEvent(el: HTMLDivElement): string | undefined {
const transitions: { [k: string]: string } = {
transition: 'transitionend',
OTransition: 'oTransitionEnd',
MozTransition: 'transitionend',
WebkitTransition: 'webkitTransitionEnd'
function getAnimationEvent(el: HTMLDivElement): string | undefined {
const animations: { [k: string]: string } = {
animation: 'animationend',
OAnimation: 'oAnimationEnd',
MozAnimation: 'Animationend',
WebkitAnimation: 'webkitAnimationEnd'
}

for (const key of Object.keys(transitions)) {
for (const key of Object.keys(animations)) {
if (el.style[key as any] !== undefined) {
return transitions[key]
return animations[key]
}
}
return
Expand All @@ -249,5 +325,7 @@ export function createSnackbar(message: string, options?: SnackOptions) {
}

export function destroyAllSnackbars() {
return Promise.all(instances.map(instance => instance.destroy()))
let instancesArray: Snackbar[] = []
Object.values(instances).forEach(map => instancesArray.push(...map.values()))
yaodingyd marked this conversation as resolved.
Show resolved Hide resolved
return Promise.all(instancesArray.map(instance => instance.destroy()))
}
26 changes: 17 additions & 9 deletions src/style.css
Expand Up @@ -11,20 +11,18 @@

.snackbar {
position: fixed;
display: flex;
box-sizing: border-box;
left: 50%;
bottom: 24px;
bottom: 14px;
width: 344px;
margin-left: -172px;
background: #2a2a2a;
border-radius: 2px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
transform-origin: center;
color: #eee;
cursor: default;
will-change: transform;
animation: snackbar-show 300ms ease forwards 1;
transition: transform 300ms ease, opacity 300ms ease;
}

.snackbar[aria-hidden='false'] {
animation: snackbar-show 300ms ease 1;
}

.snackbar[aria-hidden='true'] {
Expand All @@ -51,7 +49,7 @@
@keyframes snackbar-show {
from {
opacity: 0;
transform: scale(0.5);
transform: translate3d(0, 100%, 0)
}
}

Expand All @@ -72,6 +70,16 @@
}
}

.snackbar--container {
display: flex;
background: #2a2a2a;
border-radius: 2px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
color: #eee;
cursor: default;
margin-bottom: 10px;
}

.snackbar--text {
flex: 1 1 auto;
padding: 16px;
Expand Down