diff --git a/CHANGELOG.md b/CHANGELOG.md index 826c28e..984e444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [0.4.0](https://github.com/tutkli/ngx-sonner/compare/v0.3.4...v0.4.0) (2024-04-01) + + +### Features + +* use new signal queries ([#4](https://github.com/tutkli/ngx-sonner/issues/4)) ([bde803e](https://github.com/tutkli/ngx-sonner/commit/bde803efb543fbbbc4a812d6ebc19d4c595cd04a)) + + +### Bug Fixes + +* blurry toasts ([#6](https://github.com/tutkli/ngx-sonner/issues/6)) ([5ba650c](https://github.com/tutkli/ngx-sonner/commit/5ba650c58ecf1fa1f839c85fe0ebe5e825a72f65)) +* preserve heights order when adding new toasts ([#8](https://github.com/tutkli/ngx-sonner/issues/8)) ([c7775a2](https://github.com/tutkli/ngx-sonner/commit/c7775a20f165bc39575afd599004e6cb3787859a)) + + +### Chores + +* remove nx cloud ([fe4a4d1](https://github.com/tutkli/ngx-sonner/commit/fe4a4d19c9b9d97f4af096e5bb909f04b0b0a777)) +* sync package-lock.json ([ead7f75](https://github.com/tutkli/ngx-sonner/commit/ead7f7597326b1f2ea166fab1fa4efe1821831f8)) +* sync package-lock.json ([bcf2f46](https://github.com/tutkli/ngx-sonner/commit/bcf2f468815d1974e621d4fd9e8918a39a8a0334)) +* update nx ([ddc83fa](https://github.com/tutkli/ngx-sonner/commit/ddc83fa76da51f9d85dd4c2aa7eeb1cf8c1f029f)) + ## [0.3.4](https://github.com/tutkli/ngx-sonner/compare/v0.3.3...v0.3.4) (2024-03-07) diff --git a/README.md b/README.md index ac20812..77cd8c3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Based on [emilkowalski](https://github.com/emilkowalski)'s React [implementation ## Angular compatibility -Make sure you are using Angular v17.2.0 or greater +Make sure you are using Angular v17.3.0 or greater ## Quick start diff --git a/apps/docs/src/app/app.component.ts b/apps/docs/src/app/app.component.ts index 9d36a8e..59acb6a 100644 --- a/apps/docs/src/app/app.component.ts +++ b/apps/docs/src/app/app.component.ts @@ -36,15 +36,9 @@ import { UsageComponent } from './components/usage.component'; - - - + + + diff --git a/apps/docs/src/app/components/code-block.component.ts b/apps/docs/src/app/components/code-block.component.ts index f637b44..b57fa22 100644 --- a/apps/docs/src/app/components/code-block.component.ts +++ b/apps/docs/src/app/components/code-block.component.ts @@ -8,7 +8,7 @@ import { inject, input, signal, - ViewChild, + viewChild, } from '@angular/core'; import hljs from 'highlight.js/lib/core'; import javascript from 'highlight.js/lib/languages/javascript'; @@ -138,7 +138,7 @@ export class CodeBlockComponent { code = input.required(); _class = input('', { alias: 'class' }); - @ViewChild('codeElement') codeElement!: ElementRef; + codeElement = viewChild.required>('codeElement'); copying = signal(false); cannotDetectLanguage = computed( @@ -168,9 +168,7 @@ export class CodeBlockComponent { constructor() { effect(() => { - if (this.codeElement?.nativeElement) { - this.codeElement.nativeElement.innerHTML = this.highlightedCode(); - } + this.codeElement().nativeElement.innerHTML = this.highlightedCode(); }); } diff --git a/apps/docs/src/app/components/expand.component.ts b/apps/docs/src/app/components/expand.component.ts index 8f91712..bf67e70 100644 --- a/apps/docs/src/app/components/expand.component.ts +++ b/apps/docs/src/app/components/expand.component.ts @@ -2,9 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, - EventEmitter, - input, - Output, + model, } from '@angular/core'; import { toast } from 'ngx-sonner'; import { CodeBlockComponent } from './code-block.component'; @@ -41,8 +39,7 @@ import { CodeBlockComponent } from './code-block.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ExpandComponent { - expand = input.required(); - @Output() expandChange = new EventEmitter(); + expand = model.required(); expandSnippet = computed( () => `` @@ -52,13 +49,13 @@ export class ExpandComponent { toast('Event has been created', { description: 'Monday, January 3rd at 6:00pm', }); - this.expandChange.emit(true); + this.expand.set(true); } collapseToasts() { toast('Event has been created', { description: 'Monday, January 3rd at 6:00pm', }); - this.expandChange.emit(false); + this.expand.set(false); } } diff --git a/apps/docs/src/app/components/other.component.ts b/apps/docs/src/app/components/other.component.ts index 15ea4ba..a3e4c1b 100644 --- a/apps/docs/src/app/components/other.component.ts +++ b/apps/docs/src/app/components/other.component.ts @@ -2,9 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, - EventEmitter, - input, - Output, + model, signal, } from '@angular/core'; import { toast } from 'ngx-sonner'; @@ -39,11 +37,8 @@ import { TestComponent } from './test.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class OtherComponent { - closeButton = input.required(); - @Output() closeButtonChange = new EventEmitter(); - - richColors = input.required(); - @Output() richColorsChange = new EventEmitter(); + closeButton = model.required(); + richColors = model.required(); allTypes = [ { @@ -51,7 +46,7 @@ export class OtherComponent { snippet: "toast.success('Event has been created')", action: () => { toast.success('Event has been created'); - this.richColorsChange.emit(true); + this.richColors.set(true); }, }, { @@ -59,7 +54,7 @@ export class OtherComponent { snippet: "toast.error('Event has not been created')", action: () => { toast.error('Event has not been created'); - this.richColorsChange.emit(true); + this.richColors.set(true); }, }, { @@ -67,7 +62,7 @@ export class OtherComponent { snippet: "toast.info('Info')", action: () => { toast.info('Be at the area 10 minutes before the event time'); - this.richColorsChange.emit(true); + this.richColors.set(true); }, }, { @@ -75,7 +70,7 @@ export class OtherComponent { snippet: "toast.warning('Warning')", action: () => { toast.warning('Event start time cannot be earlier than 8am'); - this.richColorsChange.emit(true); + this.richColors.set(true); }, }, { @@ -87,7 +82,7 @@ export class OtherComponent { toast('Event has been created', { description: 'Monday, January 3rd at 6:00pm', }); - this.closeButtonChange.emit(!this.closeButton()); + this.closeButton.set(!this.closeButton()); }, }, { diff --git a/apps/docs/src/app/components/position.component.ts b/apps/docs/src/app/components/position.component.ts index 7ba1054..2d48bf1 100644 --- a/apps/docs/src/app/components/position.component.ts +++ b/apps/docs/src/app/components/position.component.ts @@ -2,13 +2,22 @@ import { ChangeDetectionStrategy, Component, computed, - EventEmitter, - input, - Output, + model, } from '@angular/core'; import { toast } from 'ngx-sonner'; import { CodeBlockComponent } from './code-block.component'; +const positions = [ + 'top-left', + 'top-center', + 'top-right', + 'bottom-left', + 'bottom-center', + 'bottom-right', +] as const; + +type Position = (typeof positions)[number]; + @Component({ selector: 'docs-position', standalone: true, @@ -33,36 +42,26 @@ import { CodeBlockComponent } from './code-block.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PositionComponent { - positions = [ - 'top-left', - 'top-center', - 'top-right', - 'bottom-left', - 'bottom-center', - 'bottom-right', - ] as const; - - position = input.required<(typeof this.positions)[number]>(); + protected positions = positions; - @Output() positionChange = new EventEmitter< - (typeof this.positions)[number] - >(); + position = model.required(); positionSnippet = computed( () => `` ); - showToast(position: (typeof this.positions)[number]) { + showToast(position: Position) { const toastsAmount = document.querySelectorAll( '[data-sonner-toast]' ).length; - this.positionChange.emit(position); // No need to show a toast when there is already one - if (toastsAmount > 0 && position !== this.position()) return; + if (toastsAmount === 0 || position === this.position()) { + toast('Event has been created', { + description: 'Monday, January 3rd at 6:00pm', + }); + } - toast('Event has been created', { - description: 'Monday, January 3rd at 6:00pm', - }); + this.position.set(position); } } diff --git a/apps/docs/src/app/components/test.component.ts b/apps/docs/src/app/components/test.component.ts index fdc3453..3f302e1 100644 --- a/apps/docs/src/app/components/test.component.ts +++ b/apps/docs/src/app/components/test.component.ts @@ -1,9 +1,8 @@ import { ChangeDetectionStrategy, Component, - EventEmitter, input, - Output, + output, } from '@angular/core'; @Component({ @@ -75,5 +74,5 @@ import { }) export class TestComponent { eventName = input.required(); - @Output() closeToast = new EventEmitter(); + closeToast = output(); } diff --git a/libs/ngx-sonner/README.md b/libs/ngx-sonner/README.md index 8cb9d96..390589f 100644 --- a/libs/ngx-sonner/README.md +++ b/libs/ngx-sonner/README.md @@ -6,7 +6,7 @@ Based on [emilkowalski](https://github.com/emilkowalski)'s React [implementation ## Angular compatibility -Make sure you are using Angular v17.2.0 or greater +Make sure you are using Angular v17.3.0 or greater ## Quick start diff --git a/libs/ngx-sonner/package.json b/libs/ngx-sonner/package.json index b311e4f..d564583 100644 --- a/libs/ngx-sonner/package.json +++ b/libs/ngx-sonner/package.json @@ -1,7 +1,7 @@ { "name": "ngx-sonner", "description": "An opinionated toast component for Angular.", - "version": "0.3.0", + "version": "0.0.0", "bugs": { "url": "https://github.com/tutkli/ngx-sonner/issues" }, @@ -23,8 +23,8 @@ }, "license": "MIT", "peerDependencies": { - "@angular/common": ">=17.1.0", - "@angular/core": ">=17.1.0" + "@angular/common": ">=17.3.0", + "@angular/core": ">=17.3.0" }, "dependencies": { "tslib": "^2.3.0" diff --git a/libs/ngx-sonner/src/lib/pipes/toast-position.pipe.ts b/libs/ngx-sonner/src/lib/pipes/toast-filter.pipe.ts similarity index 72% rename from libs/ngx-sonner/src/lib/pipes/toast-position.pipe.ts rename to libs/ngx-sonner/src/lib/pipes/toast-filter.pipe.ts index 51fac20..19be155 100644 --- a/libs/ngx-sonner/src/lib/pipes/toast-position.pipe.ts +++ b/libs/ngx-sonner/src/lib/pipes/toast-filter.pipe.ts @@ -1,8 +1,8 @@ import { Pipe, PipeTransform } from '@angular/core'; import { Position, ToastT } from '../types'; -@Pipe({ name: 'toastPosition', standalone: true }) -export class ToastPositionPipe implements PipeTransform { +@Pipe({ name: 'toastFilter', standalone: true }) +export class ToastFilterPipe implements PipeTransform { transform(toasts: ToastT[], index: number, position: Position): ToastT[] { return toasts.filter( toast => (!toast.position && index === 0) || toast.position === position diff --git a/libs/ngx-sonner/src/lib/state.ts b/libs/ngx-sonner/src/lib/state.ts index 6878506..5c9c08a 100644 --- a/libs/ngx-sonner/src/lib/state.ts +++ b/libs/ngx-sonner/src/lib/state.ts @@ -169,9 +169,13 @@ function createToastState() { } function addHeight(height: HeightT) { - heights.update(prev => [height, ...prev]); + heights.update(prev => [height, ...prev].sort(sortHeights)); } + const sortHeights = (a: HeightT, b: HeightT) => + toasts().findIndex(t => t.id === a.toastId) - + toasts().findIndex(t => t.id === b.toastId); + function reset() { toasts.set([]); heights.set([]); diff --git a/libs/ngx-sonner/src/lib/toast.component.ts b/libs/ngx-sonner/src/lib/toast.component.ts index 599660f..181e115 100644 --- a/libs/ngx-sonner/src/lib/toast.component.ts +++ b/libs/ngx-sonner/src/lib/toast.component.ts @@ -10,7 +10,7 @@ import { OnDestroy, signal, untracked, - ViewChild, + viewChild, } from '@angular/core'; import { cn } from './internal/cn'; import { AsComponentPipe } from './pipes/as-component.pipe'; @@ -236,8 +236,7 @@ export class ToastComponent implements AfterViewInit, OnDestroy { offsetBeforeRemove = signal(0); initialHeight = signal(0); - // viewChild.required>('toastRef') - @ViewChild('toastRef') toastRef!: ElementRef; + toastRef = viewChild.required>('toastRef'); classes: any = computed(() => ({ ...defaultClasses, @@ -304,7 +303,9 @@ export class ToastComponent implements AfterViewInit, OnDestroy { effect(() => { const heightIndex = this.heightIndex(); const toastsHeightBefore = this.toastsHeightBefore(); - untracked(() => this.offset.set(heightIndex * GAP + toastsHeightBefore)); + untracked(() => + this.offset.set(Math.round(heightIndex * GAP + toastsHeightBefore)) + ); }); effect(() => { @@ -342,7 +343,7 @@ export class ToastComponent implements AfterViewInit, OnDestroy { this.remainingTime = this.toast().duration ?? this.duration() ?? TOAST_LIFETIME; this.mounted.set(true); - const height = this.toastRef.nativeElement.getBoundingClientRect().height; + const height = this.toastRef().nativeElement.getBoundingClientRect().height; this.initialHeight.set(height); this.addHeight({ toastId: this.toast().id, height }); } @@ -406,8 +407,8 @@ export class ToastComponent implements AfterViewInit, OnDestroy { this.pointerStartRef = null; const swipeAmount = Number( - this.toastRef.nativeElement.style - .getPropertyValue('--swipe-amount') + this.toastRef() + .nativeElement.style.getPropertyValue('--swipe-amount') .replace('px', '') || 0 ); @@ -420,7 +421,7 @@ export class ToastComponent implements AfterViewInit, OnDestroy { return; } - this.toastRef.nativeElement.style.setProperty('--swipe-amount', '0px'); + this.toastRef().nativeElement.style.setProperty('--swipe-amount', '0px'); this.swiping.set(false); } @@ -436,7 +437,7 @@ export class ToastComponent implements AfterViewInit, OnDestroy { const isAllowedToSwipe = Math.abs(clampedY) > swipeStartThreshold; if (isAllowedToSwipe) { - this.toastRef.nativeElement.style.setProperty( + this.toastRef().nativeElement.style.setProperty( '--swipe-amount', `${yPosition}px` ); diff --git a/libs/ngx-sonner/src/lib/toaster.component.ts b/libs/ngx-sonner/src/lib/toaster.component.ts index 5a7a577..cf4591e 100644 --- a/libs/ngx-sonner/src/lib/toaster.component.ts +++ b/libs/ngx-sonner/src/lib/toaster.component.ts @@ -13,12 +13,12 @@ import { PLATFORM_ID, signal, untracked, - ViewChild, + viewChild, ViewEncapsulation, } from '@angular/core'; import { IconComponent } from './icon.component'; import { LoaderComponent } from './loader.component'; -import { ToastPositionPipe } from './pipes/toast-position.pipe'; +import { ToastFilterPipe } from './pipes/toast-filter.pipe'; import { toastState } from './state'; import { ToastComponent } from './toast.component'; import { Position, Theme, ToasterProps } from './types'; @@ -41,7 +41,7 @@ const GAP = 14; @Component({ selector: 'ngx-sonner-toaster', standalone: true, - imports: [ToastComponent, ToastPositionPipe, IconComponent, LoaderComponent], + imports: [ToastComponent, ToastFilterPipe, IconComponent, LoaderComponent], template: ` @if (toasts().length > 0) {
@for ( - toast of toasts() | toastPosition: $index : pos; + toast of toasts() | toastFilter: $index : pos; track toast.id ) { ('listRef'); - @ViewChild('listRef') listRef!: ElementRef; + listRef = viewChild>('listRef'); lastFocusedElementRef = signal(null); isFocusWithinRef = signal(false); @@ -239,7 +238,8 @@ export class NgxSonnerToaster implements OnDestroy { } private handleKeydown = (event: KeyboardEvent) => { - if (!this.listRef?.nativeElement) return; + const listRef = this.listRef()?.nativeElement; + if (!listRef) return; const isHotkeyPressed = this.hotKey().every( key => (event as never)[key] || event.code === key @@ -247,13 +247,13 @@ export class NgxSonnerToaster implements OnDestroy { if (isHotkeyPressed) { this.expanded.set(true); - this.listRef.nativeElement?.focus(); + listRef.focus(); } if ( event.code === 'Escape' && - (document.activeElement === this.listRef.nativeElement || - this.listRef.nativeElement?.contains(document.activeElement)) + (document.activeElement === listRef || + listRef.contains(document.activeElement)) ) { this.expanded.set(false); } diff --git a/libs/ngx-sonner/src/tests/toaster.spec.ts b/libs/ngx-sonner/src/tests/toaster.spec.ts index b285b47..3464d67 100644 --- a/libs/ngx-sonner/src/tests/toaster.spec.ts +++ b/libs/ngx-sonner/src/tests/toaster.spec.ts @@ -44,17 +44,17 @@ describe('Toaster', () => { it('should show a toast with custom duration', async () => { const { user, trigger, queryByText, detectChanges } = await setup({ - cb: toast => toast('Hello world', { duration: 300 }), + cb: toast => toast('Custom duration', { duration: 300 }), }); - expect(queryByText('Hello world')).toBeNull(); + expect(queryByText('Custom duration')).toBeNull(); await user.click(trigger); - expect(queryByText('Hello world')).not.toBeNull(); + expect(queryByText('Custom duration')).not.toBeNull(); await sleep(500); detectChanges(); - expect(queryByText('Hello world')).toBeNull(); + expect(queryByText('Custom duration')).toBeNull(); }); it('should reset duration on a toast update', async () => { diff --git a/package.json b/package.json index 0622e3b..04033e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ngx-sonner/source", - "version": "0.3.4", + "version": "0.4.0", "license": "MIT", "scripts": { "start:docs": "nx serve docs",