-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(snackbar): add new package (#19)
* feat(snackbar): add initial implementation with TypeScript configuration, tests, and documentation * feat(snackbar): implement Snackbar component with action button and close functionality * refactor(snackbar): enhance SnackbarComponent with detailed JSDoc comments for properties and methods * docs(snackbar): update README and package.json to reflect new snackbar component features and description * refactor(snackbar): consolidate utility functions and enhance logging with package name prefixes * refactor(snackbar): replace BaseElement with LightDomMixin and LoggerMixin for SnackbarComponent * refactor(snackbar): integrate logger instance into main module and utilize it in utility functions * refactor(snackbar): remove CHANGELOG.md and update package.json dependencies * refactor(snackbar): remove package tracer and update logger implementation in main module and utilities * chore(snackbar): change utils location * refactor(snackbar): remove logger from utils * refactor(snackbar): remove __package_name__ from logger * refactor(snackbar): rename snackbar to handler for createLogger * chore(snackbar): remove types from jsDoc * refactor(snackbar): rename signal to handler * chore(snackbar): import @nexim/element from workspace * chore(snackbar): change location of property comment's * refactor(snackbar): parse Duration for snackbar duration * doc(snackbar): remove unnecessary README Doc's * doc(snackbar): update duration format from milliseconds to seconds * doc(snackbar): write a example for snackbarSignal Method
- Loading branch information
Showing
10 changed files
with
1,360 additions
and
1 deletion.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# @nexim/snackbar | ||
|
||
This package provides a customizable snackbar component for displaying brief messages to users. It includes utilities for managing the snackbar's state and animations. | ||
|
||
![NPM Version](https://img.shields.io/npm/v/%40nexim%2Fsnackbar) | ||
![npm bundle size](https://img.shields.io/bundlephobia/min/%40nexim%2Fsnackbar) | ||
![Build & Lint & Test](https://github.com/the-nexim/nanolib/actions/workflows/build-lint-test.yaml/badge.svg) | ||
![NPM Downloads](https://img.shields.io/npm/dm/%40nexim%2Fsnackbar) | ||
![NPM License](https://img.shields.io/npm/l/%40nexim%2Fsnackbar) | ||
|
||
## Overview | ||
|
||
`@nexim/snackbar` is a versatile library designed to provide a customizable snackbar component for displaying brief messages to users. It includes utilities for managing the snackbar's state and animations, ensuring efficiency and scalability in high-performance projects. | ||
|
||
## Installation | ||
|
||
Install the package using npm or yarn: | ||
|
||
```sh | ||
npm install @nexim/snackbar | ||
|
||
# Or using yarn | ||
yarn add @nexim/snackbar | ||
``` | ||
|
||
## API | ||
|
||
### snackbarSignal | ||
|
||
To display a snackbar, emit the snackbarSignal with the desired options: | ||
|
||
```ts | ||
import {snackbarSignal} from '@nexim/snackbar'; | ||
|
||
snackbarSignal.notify({ | ||
content: 'This is a snackbar message', | ||
// The following properties are optional. | ||
action: { | ||
label: 'Undo', | ||
handler: () => { | ||
console.log('Action button clicked'); | ||
}, | ||
}, | ||
duration: '4s', | ||
addCloseButton: true, | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
{ | ||
"name": "@nexim/snackbar", | ||
"version": "0.0.0", | ||
"description": "A customizable snackbar component for displaying brief messages to users, with state management and animation utilities.", | ||
"keywords": [ | ||
"snackbar", | ||
"notification", | ||
"web-component", | ||
"typescript", | ||
"nexim" | ||
], | ||
"homepage": "https://github.com/the-nexim/nanolib/tree/next/packages/snackbar#readme", | ||
"bugs": { | ||
"url": "https://github.com/the-nexim/nanolib/issues" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/the-nexim/nanolib", | ||
"directory": "packages/snackbar" | ||
}, | ||
"license": "AGPL-3.0-only", | ||
"author": "S. Amir Mohammad Najafi <[email protected]> (www.njfamirm.ir)", | ||
"contributors": [ | ||
"Arash Ghardashpoor <[email protected]> (https://www.agpagp.ir)" | ||
], | ||
"type": "module", | ||
"exports": { | ||
".": { | ||
"types": "./dist/main.d.ts", | ||
"import": "./dist/main.mjs", | ||
"require": "./dist/main.cjs" | ||
} | ||
}, | ||
"main": "./dist/main.cjs", | ||
"module": "./dist/main.mjs", | ||
"types": "./dist/main.d.ts", | ||
"files": [ | ||
"**/*.{js,mjs,cjs,map,d.ts,html,md,LEGAL.txt}", | ||
"LICENSE", | ||
"!**/*.test.js", | ||
"!demo/**/*" | ||
], | ||
"scripts": { | ||
"b": "yarn run build", | ||
"build": "yarn run build:ts && yarn run build:es", | ||
"build:es": "nano-build --preset=module", | ||
"build:ts": "tsc --build", | ||
"c": "yarn run clean", | ||
"cb": "yarn run clean && yarn run build", | ||
"clean": "rm -rfv dist *.tsbuildinfo", | ||
"d": "yarn run build:es && yarn node --enable-source-maps --trace-warnings", | ||
"t": "yarn run test", | ||
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --enable-source-maps --experimental-vm-modules\" ava", | ||
"w": "yarn run watch", | ||
"watch": "yarn run watch:ts & yarn run watch:es", | ||
"watch:es": "yarn run build:es --watch", | ||
"watch:ts": "yarn run build:ts --watch --preserveWatchOutput" | ||
}, | ||
"dependencies": { | ||
"@alwatr/flux": "^4.0.2", | ||
"@alwatr/logger": "^5.0.0", | ||
"@alwatr/package-tracer": "^5.0.0", | ||
"@alwatr/parse-duration": "^5.0.0", | ||
"@alwatr/wait": "^1.1.16", | ||
"@nexim/element": "workspace:^", | ||
"lit": "^3.2.1" | ||
}, | ||
"devDependencies": { | ||
"@alwatr/nano-build": "^5.0.0", | ||
"@alwatr/type-helper": "^5.0.0", | ||
"@nexim/typescript-config": "workspace:^", | ||
"ava": "^6.2.0", | ||
"typescript": "^5.6.3" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import {waitForTimeout} from '@alwatr/wait'; | ||
import {LightDomMixin, LoggerMixin} from '@nexim/element'; | ||
import {html, LitElement, nothing, type PropertyValues, type TemplateResult} from 'lit'; | ||
import {customElement, property} from 'lit/decorators.js'; | ||
|
||
import {snackbarActionButtonClickedSignal} from './handler.js'; | ||
import {waitForNextFrame} from './utils.js'; | ||
|
||
declare global { | ||
interface HTMLElementTagNameMap { | ||
'snack-bar': SnackbarComponent; | ||
} | ||
} | ||
|
||
@customElement('snack-bar') | ||
export class SnackbarComponent extends LightDomMixin(LoggerMixin(LitElement)) { | ||
/** | ||
* The content to be displayed inside the snackbar. | ||
*/ | ||
@property({type: String}) content = ''; | ||
|
||
/** | ||
* The label for the action button. If null, the action button will not be rendered. | ||
*/ | ||
@property({type: String, attribute: 'action-button-label'}) actionButtonLabel: string | null = null; | ||
|
||
/** | ||
* Whether to add a close button to the snackbar. | ||
*/ | ||
@property({type: Boolean, attribute: 'add-close-button'}) addCloseButton = false; | ||
|
||
/** | ||
* Duration for the open and close animation in milliseconds. | ||
*/ | ||
private static openAndCloseAnimationDuration__ = 200; // ms | ||
|
||
protected override firstUpdated(changedProperties: PropertyValues): void { | ||
super.firstUpdated(changedProperties); | ||
|
||
// wait for render complete, then open the snackbar to start the opening animation | ||
waitForNextFrame().then(() => { | ||
this.setAttribute('open', ''); | ||
}); | ||
} | ||
|
||
/** | ||
* Close the snackbar and remove it from the DOM. | ||
* Waits for the closing animation to end before removing the element. | ||
*/ | ||
async close(): Promise<void> { | ||
this.logger_.logMethod?.('close'); | ||
|
||
this.removeAttribute('open'); | ||
|
||
await waitForTimeout(SnackbarComponent.openAndCloseAnimationDuration__); | ||
this.remove(); | ||
} | ||
|
||
/** | ||
* Handle the action button click event. | ||
* Sends a signal when the action button is clicked. | ||
*/ | ||
private actionButtonClickHandler__(): void { | ||
this.logger_.logMethod?.('actionButtonClickHandler__'); | ||
|
||
snackbarActionButtonClickedSignal.notify(); | ||
} | ||
|
||
/** | ||
* Render the snackbar component. | ||
*/ | ||
protected override render(): unknown { | ||
super.render(); | ||
|
||
const actionButtonHtml = this.renderActionButton__(); | ||
const closeButtonHtml = this.renderCloseButton__(); | ||
|
||
let actionButtonHandler: TemplateResult | typeof nothing = nothing; | ||
if (actionButtonHtml != nothing || closeButtonHtml != nothing) { | ||
actionButtonHandler = html`<div>${actionButtonHtml} ${closeButtonHtml}</div>`; | ||
} | ||
|
||
return [html`<span>${this.content}</span>`, actionButtonHandler]; | ||
} | ||
|
||
/** | ||
* Render the action button. | ||
*/ | ||
private renderActionButton__(): TemplateResult | typeof nothing { | ||
if (this.actionButtonLabel == null) return nothing; | ||
this.logger_.logMethodArgs?.('renderActionButton__', {actionLabel: this.actionButtonLabel}); | ||
|
||
return html` <button class="action-button" @click=${this.actionButtonClickHandler__.bind(this)}>${this.actionButtonLabel}</button> `; | ||
} | ||
|
||
/** | ||
* Render the close button. | ||
*/ | ||
private renderCloseButton__(): TemplateResult | typeof nothing { | ||
if (this.addCloseButton === false) return nothing; | ||
this.logger_.logMethod?.('renderCloseButton__'); | ||
|
||
return html` | ||
<button class="close-button" @click=${this.close.bind(this)}> | ||
<span class="alwatr-icon-font">close</span> | ||
</button> | ||
`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import {AlwatrSignal, AlwatrTrigger} from '@alwatr/flux'; | ||
import {createLogger} from '@alwatr/logger'; | ||
import {parseDuration, type Duration} from '@alwatr/parse-duration'; | ||
import {waitForTimeout} from '@alwatr/wait'; | ||
|
||
import type {SnackbarComponent} from './element.js'; | ||
|
||
const logger = createLogger(`${__package_name__}/handler`); | ||
|
||
/** | ||
* @property content - Content to be displayed in the snackbar. | ||
* @property {action} - The action button configuration. | ||
* @property action.label - The label for the action button. | ||
* @property action.handler - The handler function for the action button. | ||
* @property duration - Duration for which the snackbar is displayed. `-1` for infinite duration. | ||
* Duration for which the snackbar is displayed. | ||
* `-1` for infinite duration. | ||
* @property addCloseButton - Whether to add a close button to the snackbar. | ||
*/ | ||
export type SnackbarOptions = { | ||
content: string; | ||
action?: { | ||
label: string; | ||
handler: () => void; | ||
}; | ||
duration?: Duration; | ||
addCloseButton?: boolean; | ||
}; | ||
|
||
/** | ||
* Signal for when the snackbar action button is clicked. | ||
*/ | ||
export const snackbarActionButtonClickedSignal = new AlwatrTrigger({ | ||
name: 'snackbar-action-button-clicked', | ||
}); | ||
|
||
/** | ||
* Signal for displaying the snackbar. | ||
* | ||
* @example | ||
* import {snackbarSignal} from '@nexim/snackbar'; | ||
* | ||
* snackbarSignal.notify({ | ||
* content: 'This is a snackbar message', | ||
* // The following properties are optional. | ||
* action: { | ||
* label: 'Undo', | ||
* handler: () => { | ||
* console.log('Action button clicked'); | ||
* }, | ||
* }, | ||
* duration: '4s', | ||
* addCloseButton: true, | ||
* }); | ||
*/ | ||
export const snackbarSignal = new AlwatrSignal<SnackbarOptions>({name: 'snackbar'}); | ||
|
||
// Subscribe to the snackbar signal to show the snackbar when the signal is emitted. | ||
snackbarSignal.subscribe((options) => { | ||
showSnackbar(options); | ||
}); | ||
|
||
let closeLastSnackbar: (() => Promise<void>) | null = null; | ||
let unsubscribeActionButtonHandler: (() => void) | null = null; | ||
|
||
/** | ||
* Displays the snackbar with the given options. | ||
* @param options - Options for configuring the snackbar. | ||
*/ | ||
async function showSnackbar(options: SnackbarOptions): Promise<void> { | ||
logger.logMethodArgs?.('showSnackbar', {options}); | ||
|
||
// Parse the duration | ||
if (options.duration != null) options.duration = parseDuration(options.duration); | ||
|
||
// Set default duration if not provided | ||
options.duration = parseDuration('4s'); | ||
|
||
const element = document.createElement('snack-bar') as SnackbarComponent; | ||
|
||
element.setAttribute('content', options.content); | ||
|
||
if (options.addCloseButton === true) { | ||
element.setAttribute('add-close-button', ''); | ||
} | ||
|
||
if (options.action != null) { | ||
element.setAttribute('action-button-label', options.action.label); | ||
|
||
// Subscribe to the action button click | ||
unsubscribeActionButtonHandler = snackbarActionButtonClickedSignal.subscribe(() => { | ||
options.action!.handler(); | ||
|
||
return closeSnackbar_(); | ||
}).unsubscribe; | ||
} | ||
|
||
let closed = false; | ||
const closeSnackbar_ = async () => { | ||
if (closed === true) return; | ||
logger.logMethodArgs?.('closeSnackbar', {options}); | ||
|
||
await element.close(); | ||
unsubscribeActionButtonHandler?.(); | ||
closed = true; | ||
}; | ||
|
||
// Close the last snackbar if it exists | ||
await closeLastSnackbar?.(); | ||
closeLastSnackbar = closeSnackbar_; | ||
document.body.appendChild(element); | ||
|
||
// Set a timeout to close the snackbar if duration is not infinite | ||
if (options.duration !== -1) { | ||
waitForTimeout(parseDuration(options.duration)).then(closeSnackbar_); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import {waitForAnimationFrame, waitForTimeout} from '@alwatr/wait'; | ||
|
||
/** | ||
* Waits for the next frame to ensure the DOM has been fully calculated. | ||
* This minimizes the chance that querying the DOM will cause a costly reflow. | ||
* | ||
* This function uses `requestAnimationFrame` to schedule code to run immediately before the repaint, | ||
* followed by `setTimeout` with a delay of 0 to execute code as soon as possible after the repaint. | ||
* | ||
* @see https://stackoverflow.com/a/47184426 | ||
*/ | ||
export function waitForNextFrame(): Promise<void> { | ||
return new Promise((resolve) => { | ||
waitForAnimationFrame().then(() => { | ||
waitForTimeout(0).then(resolve); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import test from 'ava'; | ||
|
||
// empty test | ||
test('empty test', (test) => { | ||
test.pass(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import {packageTracer} from '@alwatr/package-tracer'; | ||
|
||
__dev_mode__: packageTracer.add(__package_name__, __package_version__); | ||
|
||
export * from './lib/element.js'; | ||
export * from './lib/handler.js'; |
Oops, something went wrong.