From 804b898d2aad414657b752cfabbdb02dc771d53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerry=2EQin=20=E7=A7=A6=E5=88=A9=E6=9D=B0?= Date: Mon, 27 May 2024 10:46:58 +0800 Subject: [PATCH 1/2] feat(marquee): add fixed for marquee --- docs/example/Marquee/demos/fixedUsage.tsx | 12 +++++ docs/example/Marquee/index.md | 2 + packages/banana/src/marquee/index.styles.ts | 8 +++ packages/banana/src/marquee/index.test.ts | 40 +++++++++++++++ packages/banana/src/marquee/index.ts | 56 ++++++++++++++++++++- packages/banana/src/select/index.test.ts | 8 +-- 6 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 docs/example/Marquee/demos/fixedUsage.tsx diff --git a/docs/example/Marquee/demos/fixedUsage.tsx b/docs/example/Marquee/demos/fixedUsage.tsx new file mode 100644 index 00000000..71790528 --- /dev/null +++ b/docs/example/Marquee/demos/fixedUsage.tsx @@ -0,0 +1,12 @@ +/** + * title: 短文案固定 + * description: 通过 `fixed` 属性可以控制,短文案固定、长文案滚动效果。 + */ + +import { Marquee } from '@banana-ui/react'; + +export default function BasicUsage() { + const content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; + + return ; +} diff --git a/docs/example/Marquee/index.md b/docs/example/Marquee/index.md index dc209b1c..6a6f152e 100644 --- a/docs/example/Marquee/index.md +++ b/docs/example/Marquee/index.md @@ -19,6 +19,7 @@ demo: + ## 属性 - Attributes & Properties @@ -28,6 +29,7 @@ demo: | color | 跑马灯的文本颜色 | `string` | '' | | duration | 滚动时长(单位:s) | `number` | 20 | | pauseWhenHover
(pause-when-hover) | 鼠标悬停时是否暂停 | `boolean` | false | +| fixed | 短文案固定 | `boolean` | false | ## CSS Parts diff --git a/packages/banana/src/marquee/index.styles.ts b/packages/banana/src/marquee/index.styles.ts index 1713474d..d4cbe323 100644 --- a/packages/banana/src/marquee/index.styles.ts +++ b/packages/banana/src/marquee/index.styles.ts @@ -15,12 +15,20 @@ export default [ .marquee { overflow: hidden; background-color: var(--banana-marquee-background-color); + + display: flex; + flex-direction: row; + align-items: center; } .content { + overflow: hidden; display: inline-block; + flex: 0 0 auto; white-space: nowrap; animation: marquee var(--banana-marquee-duration) linear infinite; + min-width: 100%; + animation-play-state: var(--banana-marquee-fixed); } @media (any-hover: hover) { diff --git a/packages/banana/src/marquee/index.test.ts b/packages/banana/src/marquee/index.test.ts index c1572811..479b53d0 100644 --- a/packages/banana/src/marquee/index.test.ts +++ b/packages/banana/src/marquee/index.test.ts @@ -104,4 +104,44 @@ describe('b-marquee', () => { expect(animationPlayState3).to.equal('running'); }); }); + + describe('custom fixed', () => { + it('should set the fixed when provided a boolean', async () => { + const element = await fixture(html``); + const fixed = window.getComputedStyle(element).getPropertyValue('--banana-marquee-fixed'); + expect(fixed).to.equal('paused'); + }); + + it('pauses animation when content width is less than marquee width', async () => { + const element = await fixture(html``); + + // Simulate content width being less than marquee width + element._mainContent!.getBoundingClientRect = () => ({ width: 50 } as DOMRect); + element._marquee!.getBoundingClientRect = () => ({ width: 100 } as DOMRect); + + // 强制调用私有方法 + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (element as any)?._calculateWidth(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect((element as any)._animationPlayState).to.equal('paused'); + expect(element.style.getPropertyValue('--banana-marquee-fixed')).to.equal('paused'); + }); + + it('resumes animation when content width is greater than marquee width', async () => { + const element = await fixture(html``); + + // Simulate content width being greater than marquee width + element._mainContent!.getBoundingClientRect = () => ({ width: 200 } as DOMRect); + element._marquee!.getBoundingClientRect = () => ({ width: 100 } as DOMRect); + + // 强制调用私有方法 + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (element as any)?._calculateWidth(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect((element as any)._animationPlayState).to.equal('running'); + expect(element.style.getPropertyValue('--banana-marquee-fixed')).to.equal('running'); + }); + }); }); diff --git a/packages/banana/src/marquee/index.ts b/packages/banana/src/marquee/index.ts index db067057..0fa1b08b 100644 --- a/packages/banana/src/marquee/index.ts +++ b/packages/banana/src/marquee/index.ts @@ -1,11 +1,12 @@ import { CSSResultGroup, html, LitElement, PropertyValueMap } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import styles from './index.styles'; @customElement('b-marquee') export default class BMarquee extends LitElement { static styles?: CSSResultGroup = styles; + private resizeObserver: ResizeObserver | undefined; connectedCallback() { super.connectedCallback(); @@ -13,6 +14,9 @@ export default class BMarquee extends LitElement { disconnectedCallback() { super.disconnectedCallback(); + // 停止观察元素的尺寸变化 + this._marquee && this.resizeObserver?.unobserve(this._marquee); + this._mainContent && this.resizeObserver?.unobserve(this._mainContent); } @property() @@ -28,6 +32,49 @@ export default class BMarquee extends LitElement { @property({ type: Boolean, attribute: 'pause-when-hover' }) pauseWhenHover = false; + @property({ type: Boolean }) + fixed = false; + + private _animationPlayState: 'running' | 'paused' = 'running'; + + @query('.marquee') + _marquee: HTMLDivElement | undefined; + + @query('#main-content') + _mainContent: HTMLDivElement | undefined; + + firstUpdated() { + // 观察元素的尺寸变化 + if (this._marquee && this._mainContent && this.fixed) { + this.resizeObserver = new ResizeObserver(() => this._calculateWidth()); + + this.resizeObserver?.observe(this._marquee); + this.resizeObserver?.observe(this._mainContent); + } + } + + private _calculateWidth() { + if (this._marquee && this._mainContent && this.fixed) { + const marqueeWidth = this._marquee.getBoundingClientRect().width; + const contentWidth = this._mainContent.getBoundingClientRect().width; + + if (contentWidth > marqueeWidth) { + this._animationPlayState = 'running'; + this._setStyleFixed(); + } else { + if (this._animationPlayState === 'running') { + // 暂停的时候等上一次的动画是否结束 结束后才暂停 这样比较友好 + this._animationPlayState = 'paused'; + this._mainContent.addEventListener('animationiteration', this._setStyleFixed.bind(this), { once: true }); + } + } + } + } + + private _setStyleFixed() { + this.style.setProperty('--banana-marquee-fixed', this._animationPlayState); + } + protected willUpdate(_changedProperties: PropertyValueMap): void { if (_changedProperties.has('color')) { const color = this.color ?? ''; @@ -38,6 +85,12 @@ export default class BMarquee extends LitElement { const duration = this.duration; this.style.setProperty('--banana-marquee-duration', `${duration}s`); } + + if (_changedProperties.has('fixed')) { + const fixed = this.fixed; + this._animationPlayState = fixed ? 'paused' : 'running'; + this._setStyleFixed(); + } } render() { @@ -49,6 +102,7 @@ export default class BMarquee extends LitElement { 'marquee--pause-when-hover': this.pauseWhenHover, })} > +
${this.content}
${this.content}
`; diff --git a/packages/banana/src/select/index.test.ts b/packages/banana/src/select/index.test.ts index fccee0af..78ab21f2 100644 --- a/packages/banana/src/select/index.test.ts +++ b/packages/banana/src/select/index.test.ts @@ -375,7 +375,7 @@ describe('b-select', () => { element.open = true; const clearIcon = element.shadowRoot?.querySelector('.clear-icon-container'); // mouse event - clearIcon.dispatchEvent(new MouseEvent('click')); + clearIcon?.dispatchEvent(new MouseEvent('click')); expect(element.value).to.equal(''); // should close the listbox. expect(element.open).to.equal(false); @@ -390,7 +390,7 @@ describe('b-select', () => { `); multipleElement.open = true; const multipleClearIcon = multipleElement.shadowRoot?.querySelector('.clear-icon-container'); - multipleClearIcon.dispatchEvent(new MouseEvent('click')); + multipleClearIcon?.dispatchEvent(new MouseEvent('click')); expect(multipleElement.value).to.deep.equal([]); // should close the listbox. expect(multipleElement.open).to.equal(false); @@ -406,7 +406,7 @@ describe('b-select', () => { `); element.open = true; const clearIcon = element.shadowRoot?.querySelector('.clear-icon-container'); - clearIcon.dispatchEvent(new MouseEvent('click')); + clearIcon?.dispatchEvent(new MouseEvent('click')); expect(element.open).to.equal(true); }); }); @@ -553,7 +553,7 @@ describe('b-select', () => { await element.updateComplete; const clearIcon = element.shadowRoot?.querySelector('.clear-icon-container'); - clearIcon.dispatchEvent(new MouseEvent('click')); + clearIcon?.dispatchEvent(new MouseEvent('click')); await element.updateComplete; From d728fdba5f270ccce72083f6b30ec5549d8783b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerry=2EQin=20=E7=A7=A6=E5=88=A9=E6=9D=B0?= Date: Wed, 7 Aug 2024 15:16:17 +0800 Subject: [PATCH 2/2] feat(marquee): add fixed for marquee --- packages/banana-react/CHANGELOG.md | 7 +++ packages/banana-react/package.json | 2 +- packages/banana/CHANGELOG.md | 6 ++ packages/banana/package.json | 2 +- packages/banana/src/marquee/index.styles.ts | 15 +++-- packages/banana/src/marquee/index.test.ts | 22 +++---- packages/banana/src/marquee/index.ts | 70 ++++++++++----------- public/Marquee/fixedUsage.html | 2 + 8 files changed, 70 insertions(+), 56 deletions(-) create mode 100644 public/Marquee/fixedUsage.html diff --git a/packages/banana-react/CHANGELOG.md b/packages/banana-react/CHANGELOG.md index 68726f79..21cf5818 100644 --- a/packages/banana-react/CHANGELOG.md +++ b/packages/banana-react/CHANGELOG.md @@ -1,5 +1,12 @@ # @banana/banana-react +## 1.19.4 + +### Patch Changes + +- Updated dependencies + - @banana-ui/banana@1.19.4 + ## 1.19.3 ### Patch Changes diff --git a/packages/banana-react/package.json b/packages/banana-react/package.json index 3ab7467c..444fafea 100644 --- a/packages/banana-react/package.json +++ b/packages/banana-react/package.json @@ -1,6 +1,6 @@ { "name": "@banana-ui/react", - "version": "1.19.3", + "version": "1.19.4", "description": "React components for Banana UI", "keywords": [ "web components", diff --git a/packages/banana/CHANGELOG.md b/packages/banana/CHANGELOG.md index f7629c72..55308255 100644 --- a/packages/banana/CHANGELOG.md +++ b/packages/banana/CHANGELOG.md @@ -1,5 +1,11 @@ # banana-ui +## 1.19.4 + +### Patch Changes + +- add fixed for marquee + ## 1.19.3 ### Patch Changes diff --git a/packages/banana/package.json b/packages/banana/package.json index 6d272f02..cb214b8e 100644 --- a/packages/banana/package.json +++ b/packages/banana/package.json @@ -1,6 +1,6 @@ { "name": "@banana-ui/banana", - "version": "1.19.3", + "version": "1.19.4", "description": "An UI library of web components can be used in any framework", "keywords": [ "web components", diff --git a/packages/banana/src/marquee/index.styles.ts b/packages/banana/src/marquee/index.styles.ts index d4cbe323..ec562472 100644 --- a/packages/banana/src/marquee/index.styles.ts +++ b/packages/banana/src/marquee/index.styles.ts @@ -21,14 +21,21 @@ export default [ align-items: center; } - .content { + .content-normal { overflow: hidden; display: inline-block; flex: 0 0 auto; white-space: nowrap; animation: marquee var(--banana-marquee-duration) linear infinite; - min-width: 100%; - animation-play-state: var(--banana-marquee-fixed); + transform: translateX(var(--banana-marquee-width, 100%)); + } + + .content-fixed { + overflow: hidden; + display: inline-block; + flex: 0 0 auto; + white-space: nowrap; + transform: translateX(0); } @media (any-hover: hover) { @@ -39,7 +46,7 @@ export default [ @keyframes marquee { 0% { - transform: translateX(0); + transform: translateX(var(--banana-marquee-width, 100%)); } 100% { diff --git a/packages/banana/src/marquee/index.test.ts b/packages/banana/src/marquee/index.test.ts index 479b53d0..a6629ce4 100644 --- a/packages/banana/src/marquee/index.test.ts +++ b/packages/banana/src/marquee/index.test.ts @@ -106,42 +106,40 @@ describe('b-marquee', () => { }); describe('custom fixed', () => { - it('should set the fixed when provided a boolean', async () => { - const element = await fixture(html``); - const fixed = window.getComputedStyle(element).getPropertyValue('--banana-marquee-fixed'); - expect(fixed).to.equal('paused'); - }); - it('pauses animation when content width is less than marquee width', async () => { const element = await fixture(html``); // Simulate content width being less than marquee width - element._mainContent!.getBoundingClientRect = () => ({ width: 50 } as DOMRect); element._marquee!.getBoundingClientRect = () => ({ width: 100 } as DOMRect); + element._content!.getBoundingClientRect = () => ({ width: 50 } as DOMRect); // 强制调用私有方法 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (element as any)?.firstUpdated(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (element as any)?._calculateWidth(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect((element as any)._animationPlayState).to.equal('paused'); - expect(element.style.getPropertyValue('--banana-marquee-fixed')).to.equal('paused'); + expect((element as any)._isNormal).to.equal(false); + expect(element.style.getPropertyValue('--banana-marquee-width')).to.equal('100px'); }); it('resumes animation when content width is greater than marquee width', async () => { const element = await fixture(html``); // Simulate content width being greater than marquee width - element._mainContent!.getBoundingClientRect = () => ({ width: 200 } as DOMRect); element._marquee!.getBoundingClientRect = () => ({ width: 100 } as DOMRect); + element._content!.getBoundingClientRect = () => ({ width: 200 } as DOMRect); // 强制调用私有方法 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (element as any)?.firstUpdated(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (element as any)?._calculateWidth(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect((element as any)._animationPlayState).to.equal('running'); - expect(element.style.getPropertyValue('--banana-marquee-fixed')).to.equal('running'); + expect((element as any)._isNormal).to.equal(true); + expect(element.style.getPropertyValue('--banana-marquee-width')).to.equal('100px'); }); }); }); diff --git a/packages/banana/src/marquee/index.ts b/packages/banana/src/marquee/index.ts index 0fa1b08b..a4b249a9 100644 --- a/packages/banana/src/marquee/index.ts +++ b/packages/banana/src/marquee/index.ts @@ -1,5 +1,5 @@ import { CSSResultGroup, html, LitElement, PropertyValueMap } from 'lit'; -import { customElement, property, query } from 'lit/decorators.js'; +import { customElement, property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import styles from './index.styles'; @@ -16,7 +16,6 @@ export default class BMarquee extends LitElement { super.disconnectedCallback(); // 停止观察元素的尺寸变化 this._marquee && this.resizeObserver?.unobserve(this._marquee); - this._mainContent && this.resizeObserver?.unobserve(this._mainContent); } @property() @@ -32,47 +31,44 @@ export default class BMarquee extends LitElement { @property({ type: Boolean, attribute: 'pause-when-hover' }) pauseWhenHover = false; - @property({ type: Boolean }) + @property({ type: Boolean, reflect: true }) fixed = false; - private _animationPlayState: 'running' | 'paused' = 'running'; + // 判断是否是 normal还是fixed (fixed为true时,才生效) + @state() + _isNormal = true; @query('.marquee') _marquee: HTMLDivElement | undefined; - @query('#main-content') - _mainContent: HTMLDivElement | undefined; + @query('.content') + _content: HTMLDivElement | undefined; firstUpdated() { + this._setBananaMarqueeWidth(); + // 观察元素的尺寸变化 - if (this._marquee && this._mainContent && this.fixed) { + if (this._marquee) { this.resizeObserver = new ResizeObserver(() => this._calculateWidth()); this.resizeObserver?.observe(this._marquee); - this.resizeObserver?.observe(this._mainContent); } } private _calculateWidth() { - if (this._marquee && this._mainContent && this.fixed) { + // marquee的宽度变化了 重新设置marquee的宽度 + this._setBananaMarqueeWidth(); + + if (this._marquee && this._content && this.fixed) { const marqueeWidth = this._marquee.getBoundingClientRect().width; - const contentWidth = this._mainContent.getBoundingClientRect().width; - - if (contentWidth > marqueeWidth) { - this._animationPlayState = 'running'; - this._setStyleFixed(); - } else { - if (this._animationPlayState === 'running') { - // 暂停的时候等上一次的动画是否结束 结束后才暂停 这样比较友好 - this._animationPlayState = 'paused'; - this._mainContent.addEventListener('animationiteration', this._setStyleFixed.bind(this), { once: true }); - } - } + const contentWidth = this._content.getBoundingClientRect().width; + + this._isNormal = contentWidth > marqueeWidth; } } - private _setStyleFixed() { - this.style.setProperty('--banana-marquee-fixed', this._animationPlayState); + private _setBananaMarqueeWidth() { + this.style.setProperty('--banana-marquee-width', `${this._marquee!.getBoundingClientRect().width}px`); } protected willUpdate(_changedProperties: PropertyValueMap): void { @@ -85,25 +81,23 @@ export default class BMarquee extends LitElement { const duration = this.duration; this.style.setProperty('--banana-marquee-duration', `${duration}s`); } - - if (_changedProperties.has('fixed')) { - const fixed = this.fixed; - this._animationPlayState = fixed ? 'paused' : 'running'; - this._setStyleFixed(); - } } render() { + const marqueeClass = classMap({ + marquee: true, + 'marquee--pause-when-hover': this.pauseWhenHover, + }); + + const contentClass = classMap({ + content: true, + 'content-normal': this._isNormal, + 'content-fixed': !this._isNormal, + }); + return html` -
-
${this.content}
-
${this.content}
+
+
${this.content}
`; } diff --git a/public/Marquee/fixedUsage.html b/public/Marquee/fixedUsage.html new file mode 100644 index 00000000..2a4f6b09 --- /dev/null +++ b/public/Marquee/fixedUsage.html @@ -0,0 +1,2 @@ + \ No newline at end of file