Skip to content

Commit

Permalink
Added SnackBar component
Browse files Browse the repository at this point in the history
  • Loading branch information
SweetDealer committed Jan 9, 2024
1 parent 7e696a2 commit 20e6dc5
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 8 deletions.
111 changes: 111 additions & 0 deletions src/SnackBar/SnackBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {safeText} from "../utils.js";

export default class SnackBar {
/** @param {string} - needed in order to be able to create multiple snackbars on a page */
#snackBarId;
/** @param {number} - needed in order to delete the previous timeout when a new one appears */
#timeoutId;
/** @param {HTMLElement} */
#snackBarEl;

/** @param {string} id */
constructor(id = 'snackBar') {
this.#snackBarId = id;
}

/**
* SnackBar is a tiny notification displayed at the bottom of the screen. It's allowed to have only one SnackBar in
* the same time. Creating a new SnackBar will remove the old one. Default living time is 15 sec after which
* SnackBar disappears.
* Properties:
* - msgText - main text (e.g. Smth was deleted)
* - btnText - OK button text (e.g. Undo)
* - btnCb - function to be called when OK btn pressed
* - ttl - aka TimeToLive in milliseconds
* @param {{msgText:string, btnText:string, btnCb:function, ttl:number}} prop
*/
show(prop) {
SnackBar.#validateProperties(prop);
this.#removeExistingIfNeeded();
this.#snackBarEl = this.#createSnackBarElement(this.#snackBarId, prop);
this.#addEventListeners(prop.btnCb);
this.#timeoutId = setTimeout(() => {
this.#removeEl()
}, prop.ttl || 15 * 1000);
}

#removeExistingIfNeeded(){
const existing = document.getElementById(`${this.#snackBarId}`);
if (existing)
existing.remove();
if (this.#timeoutId)
window.clearTimeout(this.#timeoutId)
}

/** @param {Function} callback */
#addEventListeners(callback) {
const okButton = this.#snackBarEl.querySelector('[js-ok]');
if (okButton) {
okButton.addEventListener('click', () => {
this.#removeEl();
callback()
})
}
const closeButton = this.#snackBarEl.querySelector('[js-close]');
closeButton.addEventListener('click', () => {this.#removeEl()})
}

#removeEl() {
this.#snackBarEl.remove();
const containerEl = SnackBar.#getSnackBarContainerEl();
if (containerEl.children.length === 0)
containerEl.remove();
}

/**
* @param {string} id
* @param {{msgText:string, btnText:string, btnCb:function, ttl:number}} prop
* @return {HTMLDivElement}
*/
#createSnackBarElement(id, prop) {
const containerEl = SnackBar.#getSnackBarContainerEl();
containerEl.insertAdjacentHTML("beforeend", SnackBar.#htmlTemplate(prop.msgText, prop.btnText, this.#snackBarId));
return containerEl.lastElementChild;
}

/**
* @param {string} messageText
* @param {string?} buttonText
* @param {string} elementId
* @return {string}
*/
static #htmlTemplate(messageText, buttonText, elementId) {
return `
<div id="${elementId}" class="snackbar">
<div class="snackbar__label">${safeText(messageText)}</div>
<div class="snackbar__buttons">
${buttonText ? `<button class="snackbar__button-ok" js-ok>${safeText(buttonText)}</button>` : ''}
<button class="snackbar__button-close material-symbols-outlined" js-close title="Close">close</button>
</div>
</div>`
}

/** @param {{msgText:string, btnText:string, btnCb:function, ttl:number}} prop */
static #validateProperties(prop) {
if (!prop) throw new Error('No SnackBar properties');
if (!prop.msgText) throw new Error('Empty SnackBar message text');
if (prop.btnText && !prop.btnCb) throw new Error('No callback for SnackBar button');
if (!prop.btnText && prop.btnCb) throw new Error('No SnackBar button text');
if (prop.btnCb && (typeof prop.btnCb !== "function")) throw new Error('Callback for SnackBar button is not a function');
if (prop.ttl && (typeof prop.ttl !== "number")) throw new Error('TTL is not a number');
}

static #getSnackBarContainerEl(){
let container = document.body.querySelector(".snackbar-menu");
if (!container){
document.body.insertAdjacentHTML("beforeend", "<div class='snackbar-menu'/>");
container = document.body.lastElementChild;
}
return container;
}
}
94 changes: 94 additions & 0 deletions src/SnackBar/SnackBar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@

.snackbar-menu {
--snackbar-background-color: rgb(51, 51, 51);
--snackbar-background-color-hover: rgb(84, 84, 84);
--snackbar-text-label-color: rgba(255, 255, 255, 0.87);
--snackbar-text-action-color: rgb(187, 134, 252);
--snackbar-text-action-disabled-color: rgb(140, 140, 140);
z-index : 8;
margin : 0.5rem;
position : fixed;
right : 0;
bottom : 0;
left : 0;
align-items : center;
justify-content : center;
box-sizing : border-box;
display : flex;
flex-direction: column;
}

.snackbar {
margin: 0.5rem;
background-color : var(--snackbar-background-color);
min-width : 344px;
box-shadow : var(--box-shadow);
max-width : 672px;
border-radius : 4px;
display : flex;
align-items : center;
justify-content : flex-start;
}

@media (max-width : 480px), (max-width : 344px){
.snackbar__surface{
min-width : 100%;
}
}

.snackbar__label{
color : var(--snackbar-text-label-color);
}

.snackbar__label{
font-size : 0.875rem;
line-height : 1.25rem;
font-weight : 400;
letter-spacing : 0.012rem;
flex-grow : 1;
box-sizing : border-box;
margin : 0;
padding : 1rem;
}


.snackbar__buttons {
margin-left : 0.5rem;
margin-right : 0.5rem;
display : flex;
flex-shrink : 0;
align-items : center;
box-sizing : border-box;
}

.snackbar__button-ok {
background: var(--snackbar-background-color);
color : var(--snackbar-text-action-color);
border: none;
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
margin-right: 0.5rem;
}

.snackbar__button-ok:disabled, .snackbar__button-close:disabled {
border: none;
color: var(--snackbar-text-action-disabled-color);
pointer-events: none;
}

.snackbar__button-ok:hover, .snackbar__button-close:hover{
background: var(--snackbar-background-color-hover);
}

.snackbar__button-close {
background: var(--snackbar-background-color);
color : var(--snackbar-text-label-color);
border: none;
cursor: pointer;
font-size: 1rem;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
padding: 0;
}
56 changes: 52 additions & 4 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,18 @@ <h1>Elsci Essential Components</h1>
</li>
</ul>
<a href="#edit-text">Read more</a></div>
</div>
<h2 id="text-input">Text Input</h2>

<hr>
<div class="input-element__row"><h3>SnackBar</h3>
<ul class="input-element__list">
<li class="input-element__item"><button js-showSnackBar>Show SnackBar</button><br></li>
</ul>

<a href="#snackbar">Read more</a>
</div>
<hr>

<h2 id="text-input">Text Input</h2>

<div class="input-element__description">
<div class="input-element__description-text">
Expand Down Expand Up @@ -219,18 +229,39 @@ <h2 id="typeahead-input">Typeahead Input</h2>

typeAheadInput.options = typeAheadInputOptions;
typeAheadInput.value = typeAheadInputOptions[0];
</code></pre>
</code></pre></div>
<h2 id="edit-text">EditText</h2>
<div class="input-element__description">
<div>
Displayed as regular text(underlined) but allows editing it's value
</div>
<pre class="input-element__description-example">
<code class="language-html">
&lt;edit-text name="Precision" required type="number" min="1" max="100"&gt;&lt;/edit-text&gt;
&lt;edit-text name="Precision" required type="number" min="1" max="100"&gt;&lt;/edit-text&gt;
</code>
</pre>
</div>
<h2 id="snackbar">SnackBar</h2>
<div class="input-element__description">
<div>
This component shows small element in the bottom of the page.<br>
<ul>Properties:
<li>- msgText - main text (e.g. Smth was deleted)</li>
<li>- btnText - OK button text (e.g. Undo)</li>
<li>- btnCb - function to be called when OK btn pressed</li>
<li>- ttl - aka TimeToLive in milliseconds</li>
</ul>
You can have multiple snackBars in one page if create components with different ids
</div>
<pre class="input-element__description-example">
<code class="language-js">
const snackBar = new SnackBar('id');
snackBar.show({msgText:'SnackBar text', btnText:'Action', btnCb: ()=> window.alert("Hello world!"), ttl: 15 * 1000})
</code>
</pre>
</div>

</div>
</div>
<script>
window.onload = () => {
Expand Down Expand Up @@ -262,6 +293,23 @@ <h2 id="edit-text">EditText</h2>
typeAheadInputActive.options = typeAheadInputOptions;
typeAheadInputActive.value = typeAheadInputOptions[0];


/* SnackBar */
const snackBarButton = document.querySelector('[js-showSnackBar]');
function showSnackBar() {
new ui.SnackBar().show({msgText:'Button was removed', btnText:'UNDO', btnCb: ()=> showButton(), ttl: 15 * 1000})
}
function hideButton(){
snackBarButton.classList.add('visually-hidden')
}
function showButton(){
snackBarButton.classList.remove('visually-hidden')
}
snackBarButton.addEventListener('click', ()=>{
hideButton();
showSnackBar();
});

//This code adds color to html and css examples
hljs.highlightAll();
};
Expand Down
15 changes: 11 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export { default as TextInput } from './TextInput/TextInput.js';
export { default as TypeAheadInput } from './TypeAheadInput/TypeAheadInput.js';
export { default as SelectInput } from './SelectInput/SelectInput.js'
export { default as EditText } from './EditText/EditText.js'
import SnackBar from "./SnackBar/SnackBar.js";

export {default as TextInput} from './TextInput/TextInput.js';
export {default as TypeAheadInput} from './TypeAheadInput/TypeAheadInput.js';
export {default as SelectInput} from './SelectInput/SelectInput.js';
export {default as EditText} from './EditText/EditText.js';
export {default as SnackBar} from './SnackBar/SnackBar.js';
if (window)
window.ui = {
SnackBar
}
1 change: 1 addition & 0 deletions src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@import './SelectInput/SelectInput.scss';
@import './TypeAheadInput/TypeAheadInput.scss';
@import './EditText/EditText.scss';
@import './SnackBar/SnackBar.scss';

.visually-hidden {
display: none !important;
Expand Down
Loading

0 comments on commit 20e6dc5

Please sign in to comment.