Skip to content

Commit

Permalink
snackbar: Make snackbars stackable. fixed #350
Browse files Browse the repository at this point in the history
  • Loading branch information
zdhxiong committed Oct 21, 2024
1 parent 7288639 commit d699173
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 71 deletions.
200 changes: 135 additions & 65 deletions packages/mdui/src/components/snackbar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,28 @@ import { animateTo, stopAnimations } from '@mdui/shared/helpers/animate.js';
import { breakpoint } from '@mdui/shared/helpers/breakpoint.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { getDuration, getEasing } from '@mdui/shared/helpers/motion.js';
import { observeResize } from '@mdui/shared/helpers/observeResize.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import '@mdui/shared/icons/clear.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import '../button-icon.js';
import '../button.js';
import '../icon.js';
import { style } from './style.js';
import type { ObserveResize } from '@mdui/shared/helpers/observeResize.js';
import type { CSSResultGroup, TemplateResult } from 'lit';

// snackbar 堆叠时的数组
const stacks: {
// snackbar 高度
height: number;
// snackbar 实例
snackbar: Snackbar;
}[] = [];

// 是否重新排序中,mobile 变化时,仅重新排序一次
let reordering = false;

/**
* @summary 消息条组件
*
Expand Down Expand Up @@ -137,7 +150,15 @@ export class Snackbar extends MduiElement<SnackbarEventMap> {
})
public closeOnOutsideClick = false;

@property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
private mobile = false;

private closeTimeout!: number;
private observeResize?: ObserveResize;

public constructor() {
super();
Expand All @@ -147,23 +168,11 @@ export class Snackbar extends MduiElement<SnackbarEventMap> {

@watch('open')
private async onOpenChange() {
const isMobile = breakpoint().down('sm');
const isCenteredHorizontally = ['top', 'bottom'].includes(this.placement);

const easingLinear = getEasing(this, 'linear');
const easingEmphasizedDecelerate = getEasing(this, 'emphasized-decelerate');

const children = Array.from(
this.renderRoot.querySelectorAll<HTMLElement>('.message, .action-group'),
);

// 手机上始终使用全宽的样式,但 @media 选择器中无法使用 CSS 变量,所以使用 js 来设置样式
const commonStyle = isMobile
? { left: '1rem', right: '1rem', minWidth: 0 }
: isCenteredHorizontally
? { left: '50%' }
: {};

// 打开
// 要区分是否首次渲染,首次渲染时不触发事件,不执行动画;非首次渲染,触发事件,执行动画
if (this.open) {
Expand Down Expand Up @@ -192,38 +201,15 @@ export class Snackbar extends MduiElement<SnackbarEventMap> {
...children.map((child) => stopAnimations(child)),
]);

const duration = getDuration(this, 'medium4');
stacks.push({
height: this.clientHeight,
snackbar: this,
});
await this.reorderStack(this);

const getOpenStyle = (ident: 'start' | 'end') => {
const scaleY = `scaleY(${ident === 'start' ? 0 : 1})`;

if (isMobile) {
return { transform: scaleY };
} else {
return {
transform: [
scaleY,
isCenteredHorizontally ? 'translateX(-50%)' : '',
]
.filter((i) => i)
.join(' '),
};
}
};
const duration = getDuration(this, 'medium4');

await Promise.all([
animateTo(
this,
[
{ ...getOpenStyle('start'), ...commonStyle },
{ ...getOpenStyle('end'), ...commonStyle },
],
{
duration: hasUpdated ? duration : 0,
easing: easingEmphasizedDecelerate,
fill: 'forwards',
},
),
animateTo(
this,
[{ opacity: 0 }, { opacity: 1, offset: 0.5 }, { opacity: 1 }],
Expand Down Expand Up @@ -273,55 +259,78 @@ export class Snackbar extends MduiElement<SnackbarEventMap> {

const duration = getDuration(this, 'short4');

const getCloseStyle = (ident: 'start' | 'end') => {
const opacity = ident === 'start' ? 1 : 0;
const styles = { opacity };

if (!isMobile && isCenteredHorizontally) {
Object.assign(styles, { transform: 'translateX(-50%)' });
}

return styles;
};

await Promise.all([
animateTo(
this,
[
{ ...getCloseStyle('start'), ...commonStyle },
{ ...getCloseStyle('end'), ...commonStyle },
],
{
duration,
easing: easingLinear,
fill: 'forwards',
},
),
animateTo(this, [{ opacity: 1 }, { opacity: 0 }], {
duration,
easing: easingLinear,
fill: 'forwards',
}),
...children.map((child) =>
animateTo(
child,
[{ opacity: 1 }, { opacity: 0, offset: 0.75 }, { opacity: 0 }],
{ duration, easing: easingLinear },
{
duration,
easing: easingLinear,
},
),
),
]);

this.style.display = 'none';
this.emit('closed');

const stackIndex = stacks.findIndex((stack) => stack.snackbar === this);
stacks.splice(stackIndex, 1);
if (stacks[stackIndex]) {
await this.reorderStack(stacks[stackIndex].snackbar);
}

return;
}
}

/**
* 这两个属性变更时,需要重新排序该组件后面的 snackbar
*/
@watch('placement', true)
@watch('messageLine', true)
private async onStackChange() {
await this.reorderStack(this);
}

public override connectedCallback(): void {
super.connectedCallback();

document.addEventListener('pointerdown', this.onDocumentClick);

// 先立即计算一次,避免在首次 open 时还未计算完成
this.mobile = breakpoint().down('sm');

this.observeResize = observeResize(document.documentElement, async () => {
const mobile = breakpoint().down('sm');

if (this.mobile !== mobile) {
this.mobile = mobile;

if (!reordering) {
reordering = true;
await this.reorderStack();
reordering = false;
}
}
});
}

public override disconnectedCallback(): void {
super.disconnectedCallback();

document.removeEventListener('pointerdown', this.onDocumentClick);
window.clearTimeout(this.closeTimeout);
if (this.open) {
this.open = false;
}
this.observeResize?.unobserve();
}

protected override render(): TemplateResult {
Expand Down Expand Up @@ -363,6 +372,67 @@ export class Snackbar extends MduiElement<SnackbarEventMap> {
</div>`;
}

/**
* 重新排序 snackbar 堆叠
* @param startSnackbar 从哪个 snackbar 开始重新排列,默认从第一个开始
* @private
*/
private async reorderStack(startSnackbar?: Snackbar) {
const stackIndex = startSnackbar
? stacks.findIndex((stack) => stack.snackbar === startSnackbar)
: 0;

for (let i = stackIndex; i < stacks.length; i++) {
const stack = stacks[i];
const snackbar = stack.snackbar;

if (this.mobile) {
['top', 'bottom'].forEach((placement) => {
if (snackbar.placement.startsWith(placement)) {
const prevStacks = stacks.filter((stack, index) => {
return (
index < i && stack.snackbar.placement.startsWith(placement)
);
});
const prevHeight = prevStacks.reduce(
(prev, current) => prev + current.height,
0,
);

// @ts-ignore
snackbar.style[placement] =
`calc(${prevHeight}px + ${prevStacks.length + 1}rem)`;
snackbar.style[placement === 'top' ? 'bottom' : 'top'] = 'auto';
}
});
} else {
[
'top',
'top-start',
'top-end',
'bottom',
'bottom-start',
'bottom-end',
].forEach((placement) => {
if (snackbar.placement === placement) {
const prevStacks = stacks.filter((stack, index) => {
return index < i && stack.snackbar.placement === placement;
});
const prevHeight = prevStacks.reduce(
(prev, current) => prev + current.height,
0,
);

snackbar.style[placement.startsWith('top') ? 'top' : 'bottom'] =
`calc(${prevHeight}px + ${prevStacks.length + 1}rem)`;
snackbar.style[placement.startsWith('top') ? 'bottom' : 'top'] =
'auto';
}
});
}
}
}

/**
* 在 document 上点击时,根据条件判断是否要关闭 snackbar
*/
Expand Down
36 changes: 30 additions & 6 deletions packages/mdui/src/components/snackbar/style.less
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
align-items: center;
flex-wrap: wrap;
border-radius: var(--shape-corner);
transform: scaleY(0);
transition: transform 0ms var(--mdui-motion-easing-linear) var(--mdui-motion-duration-short4);
.min-width(320);
.max-width(576);
.padding(4, 0);
Expand All @@ -19,24 +21,46 @@

:host([placement^="top"]) {
transform-origin: top;
.top(16);
}

:host([placement^="bottom"]) {
transform-origin: bottom;
.bottom(16);
}

:host([placement="top-start"]),
:host([placement="bottom-start"]) {
:host([placement="top-start"]:not([mobile])),
:host([placement="bottom-start"]:not([mobile])) {
.left(16);
}

:host([placement="top-end"]),
:host([placement="bottom-end"]) {
:host([placement="top-end"]:not([mobile])),
:host([placement="bottom-end"]:not([mobile])) {
.right(16);
}

:host([placement="top"]:not([mobile])),
:host([placement="bottom"]:not([mobile])) {
left: 50%;
transform: scaleY(0) translateX(-50%);
}

:host([mobile]) {
min-width: 0;
.left(16);
.right(16);
}

:host([open]) {
transform: scaleY(1);
transition: top var(--mdui-motion-duration-short4) var(--mdui-motion-easing-standard),
bottom var(--mdui-motion-duration-short4) var(--mdui-motion-easing-standard),
transform var(--mdui-motion-duration-medium4) var(--mdui-motion-easing-emphasized-decelerate);
}

:host([placement="top"][open]:not([mobile])),
:host([placement="bottom"][open]:not([mobile])) {
transform: scaleY(1) translateX(-50%);
}

.message {
display: block;
.margin(10, 16);
Expand Down

0 comments on commit d699173

Please sign in to comment.