From d13672a22648883030cb0170431fad82eb81a96d Mon Sep 17 00:00:00 2001 From: Joren Broekema Date: Mon, 13 Jan 2020 10:42:13 +0100 Subject: [PATCH] feat(helpers): new package with several helpers --- packages/helpers/README.md | 30 +++ packages/helpers/index.js | 6 + packages/helpers/package.json | 38 ++++ .../renderLitAsNode/src/renderLitAsNode.js | 7 + .../test/renderLitAsNode.test.js | 39 ++++ packages/helpers/sb-action-logger.js | 3 + packages/helpers/sb-action-logger/README.md | 35 ++++ .../sb-action-logger/custom-elements.json | 46 +++++ .../sb-action-logger/src/SbActionLogger.js | 178 ++++++++++++++++++ .../stories/index.stories.mdx | 116 ++++++++++++ .../test/sb-action-logger.test.js | 125 ++++++++++++ packages/helpers/sb-locale-switcher.js | 3 + .../src/SbLocaleSwitcher.js | 31 +++ 13 files changed, 657 insertions(+) create mode 100644 packages/helpers/README.md create mode 100644 packages/helpers/index.js create mode 100644 packages/helpers/package.json create mode 100644 packages/helpers/renderLitAsNode/src/renderLitAsNode.js create mode 100644 packages/helpers/renderLitAsNode/test/renderLitAsNode.test.js create mode 100644 packages/helpers/sb-action-logger.js create mode 100644 packages/helpers/sb-action-logger/README.md create mode 100644 packages/helpers/sb-action-logger/custom-elements.json create mode 100644 packages/helpers/sb-action-logger/src/SbActionLogger.js create mode 100644 packages/helpers/sb-action-logger/stories/index.stories.mdx create mode 100644 packages/helpers/sb-action-logger/test/sb-action-logger.test.js create mode 100644 packages/helpers/sb-locale-switcher.js create mode 100644 packages/helpers/sb-locale-switcher/src/SbLocaleSwitcher.js diff --git a/packages/helpers/README.md b/packages/helpers/README.md new file mode 100644 index 000000000..021136fc0 --- /dev/null +++ b/packages/helpers/README.md @@ -0,0 +1,30 @@ +# Helpers + +[//]: # 'AUTO INSERT HEADER PREPUBLISH' + +A helpers package that contains several helpers that are used inside lion but can be used outside as well. + +These helpers are considered developer tools, not actual things to use in production. +Therefore, they may not have the same quality standards as our other packages. + +## Live Demo/Documentation + +> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/helpers) for a live demo and API documentation + +## Installation + +```bash +npm i @lion/helpers +``` + +## Usage + +Example using the sb-action-logger helper component. + +```html + + + +``` diff --git a/packages/helpers/index.js b/packages/helpers/index.js new file mode 100644 index 000000000..ac3c167ff --- /dev/null +++ b/packages/helpers/index.js @@ -0,0 +1,6 @@ +// Utilities +export { renderLitAsNode } from './renderLitAsNode/src/renderLitAsNode.js'; + +// Components +export { SbActionLogger } from './sb-action-logger/src/SbActionLogger.js'; +export { SbLocaleSwitcher } from './sb-locale-switcher/src/SbLocaleSwitcher.js'; diff --git a/packages/helpers/package.json b/packages/helpers/package.json new file mode 100644 index 000000000..80dfda2b6 --- /dev/null +++ b/packages/helpers/package.json @@ -0,0 +1,38 @@ +{ + "name": "@lion/helpers", + "version": "0.1.0", + "description": "Helpers that are used throughout lion and can be used outside", + "author": "ing-bank", + "homepage": "https://github.com/ing-bank/lion/", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/ing-bank/lion.git", + "directory": "packages/helpers" + }, + "scripts": { + "prepublishOnly": "../../scripts/npm-prepublish.js" + }, + "keywords": [ + "lion", + "web-components", + "helpers", + "action logger" + ], + "main": "index.js", + "module": "index.js", + "files": [ + "sb-action-logger", + "*.js" + ], + "dependencies": { + "@lion/core": "0.3.0" + }, + "devDependencies": { + "@open-wc/demoing-storybook": "^1.6.1", + "@open-wc/testing": "^2.3.4" + } +} diff --git a/packages/helpers/renderLitAsNode/src/renderLitAsNode.js b/packages/helpers/renderLitAsNode/src/renderLitAsNode.js new file mode 100644 index 000000000..da60eaff3 --- /dev/null +++ b/packages/helpers/renderLitAsNode/src/renderLitAsNode.js @@ -0,0 +1,7 @@ +import { render } from '@lion/core'; + +export const renderLitAsNode = litHtmlTemplate => { + const offlineRenderContainer = document.createElement('div'); + render(litHtmlTemplate, offlineRenderContainer); + return offlineRenderContainer.firstElementChild; +}; diff --git a/packages/helpers/renderLitAsNode/test/renderLitAsNode.test.js b/packages/helpers/renderLitAsNode/test/renderLitAsNode.test.js new file mode 100644 index 000000000..6cce34a80 --- /dev/null +++ b/packages/helpers/renderLitAsNode/test/renderLitAsNode.test.js @@ -0,0 +1,39 @@ +import { expect } from '@open-wc/testing'; +import { html } from '@lion/core'; +import { renderLitAsNode } from '../src/renderLitAsNode.js'; + +describe('renderLitAsNode', () => { + it('should return a matching HTMLElement (Node)', () => { + const el = renderLitAsNode(html` +
+ Link + Some text: Hello, World +
+ `); + + expect(el).to.be.instanceOf(HTMLElement); + expect(el).dom.to.equal(` +
+ Link + Some text: Hello, World +
+ `); + }); + + it('should only render and return the first (root) node in the template', () => { + const el = renderLitAsNode(html` +
+ Link + Some text: Hello, World +
+
Sibling div
+ `); + + expect(el).dom.to.equal(` +
+ Link + Some text: Hello, World +
+ `); + }); +}); diff --git a/packages/helpers/sb-action-logger.js b/packages/helpers/sb-action-logger.js new file mode 100644 index 000000000..872ef5678 --- /dev/null +++ b/packages/helpers/sb-action-logger.js @@ -0,0 +1,3 @@ +import { SbActionLogger } from './sb-action-logger/src/SbActionLogger.js'; + +window.customElements.define('sb-action-logger', SbActionLogger); diff --git a/packages/helpers/sb-action-logger/README.md b/packages/helpers/sb-action-logger/README.md new file mode 100644 index 000000000..a5ced2026 --- /dev/null +++ b/packages/helpers/sb-action-logger/README.md @@ -0,0 +1,35 @@ +# Storybook Action Logger + +[//]: # 'AUTO INSERT HEADER PREPUBLISH' + +A visual element to show action logs in Storybook demos `sb-action-logger`. + +**This is a demonstrative tool**, not a debugging tool (although it may help initially). +If you try logging complex values such as arrays, objects or promises, +you should expect to get only the string interpretation as the output in this logger. + +## Live Demo/Documentation + +> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/helpers-storybook-action-logger) for a live demo and API documentation + +## Installation + +```bash +npm i @lion/helpers +``` + +## Usage + +```html + + + +``` + +Then, with the sb-action-logger instance selected, call the `log` method on it. + +```js +myActionLoggerInstance.log('Hello, World!'); +``` diff --git a/packages/helpers/sb-action-logger/custom-elements.json b/packages/helpers/sb-action-logger/custom-elements.json new file mode 100644 index 000000000..22cb809c4 --- /dev/null +++ b/packages/helpers/sb-action-logger/custom-elements.json @@ -0,0 +1,46 @@ +{ + "version": 0, + "tags": [ + { + "name": "sb-action-logger", + "description": "A visual element to show action logs in Storybook demos", + "properties": [ + { + "name": "title", + "type": "String", + "description": "The title of action logger", + "default": "Action Logger" + } + ], + "events": [], + "slots": [], + "cssProperties": [ + { + "name": "--sb-action-logger-title-color", + "description": "Color of the title", + "type": "Color" + }, + { + "name": "--sb-action-logger-text-color", + "description": "Color of the logs' text", + "type": "Color" + }, + { + "name": "--sb-action-logger-cue-color-primary", + "description": "Primary color of the visual cue", + "type": "Color" + }, + { + "name": "--sb-action-logger-cue-color-secondary", + "description": "Secondary color of the visual cue", + "type": "Color" + }, + { + "name": "--sb-action-logger-cue-duration", + "description": "Duration of the visual cue", + "type": "Number" + } + ] + } + ] +} diff --git a/packages/helpers/sb-action-logger/src/SbActionLogger.js b/packages/helpers/sb-action-logger/src/SbActionLogger.js new file mode 100644 index 000000000..80595a9d7 --- /dev/null +++ b/packages/helpers/sb-action-logger/src/SbActionLogger.js @@ -0,0 +1,178 @@ +import { css, html, LitElement, render } from '@lion/core'; + +/** @typedef {import('lit-html').TemplateResult} TemplateResult */ + +export class SbActionLogger extends LitElement { + static get properties() { + return { + title: { type: String, reflect: true }, + __logCounter: { type: Number }, + }; + } + + static get styles() { + return css` + :host { + --sb-action-logger-title-color: black; + --sb-action-logger-text-color: black; + --sb-action-logger-cue-color-primary: #3f51b5; + --sb-action-logger-cue-color-secondary: #c5cae9; + --sb-action-logger-cue-duration: 1000ms; + + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + display: block; + } + + .header__info { + color: var(--sb-action-logger-title-color); + display: flex; + align-items: center; + padding: 16px; + font-size: 16px; + } + + .header__clear { + margin-left: 16px; + border-radius: 0px; + background-color: rgba(0, 0, 0, 0.05); + border: none; + cursor: pointer; + padding: 8px; + } + + .header__clear:hover { + background-color: rgba(0, 0, 0, 0.1); + } + + .header__title { + margin: 0; + font-weight: bold; + flex-grow: 1; + } + + .header__log-cue { + position: relative; + height: 3px; + background-color: var(--sb-action-logger-cue-color-secondary); + overflow: hidden; + } + + .header__log-cue-overlay { + position: absolute; + height: 3px; + width: 50px; + left: -50px; + background-color: var(--sb-action-logger-cue-color-primary); + } + + .header__log-cue-overlay--slide { + animation: slidethrough var(--sb-action-logger-cue-duration) ease-in; + } + + @keyframes slidethrough { + from { + left: -50px; + width: 50px; + } + + to { + left: 100%; + width: 500px; + } + } + + .logger { + overflow-y: auto; + max-height: 110px; + } + + .logger__log { + padding: 16px; + } + + .logger__log:not(:last-child) { + border-bottom: 1px solid lightgrey; + } + + .logger__log code { + color: var(--sb-action-logger-text-color); + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + } + `; + } + + constructor() { + super(); + this.title = 'Action Logger'; + this.__logCounter = 0; + } + + /** + * Renders the passed content as a node, and appends it to the logger + * Only supports simple values, will be interpreted to a String + * E.g. an Object will become '[object Object]' + * + * @param {} content Content to be logged to the action logger + */ + log(content) { + const loggerEl = this.shadowRoot.querySelector('.logger'); + const offlineRenderContainer = document.createElement('div'); + render(this._logTemplate(content), offlineRenderContainer); + // TODO: Feature, combine duplicate consecutive logs as 1 dom element and add a counter for dupes + loggerEl.appendChild(offlineRenderContainer.firstElementChild); + this.__logCounter += 1; + this.__animateCue(); + loggerEl.scrollTo({ top: loggerEl.scrollHeight, behavior: 'smooth' }); + } + + /** + * Protected getter that returns the template of a single log + * + * @return {TemplateResult} TemplateResult that uses the content passed to create a log + */ + // eslint-disable-next-line class-methods-use-this + _logTemplate(content) { + return html` +
+ ${content} +
+ `; + } + + __animateCue() { + const cueEl = this.shadowRoot.querySelector('.header__log-cue-overlay'); + cueEl.classList.remove('header__log-cue-overlay--slide'); + // This triggers browser to stop batching changes because it has to evaluate something. + // eslint-disable-next-line no-void + void this.offsetWidth; + // So that when we arrive here, the browser sees this adding as an actual 'change' + // and this means the animation gets refired. + cueEl.classList.add('header__log-cue-overlay--slide'); + } + + __clearLogs() { + const loggerEl = this.shadowRoot.querySelector('.logger'); + loggerEl.innerHTML = ''; + this.__logCounter = 0; + } + + render() { + return html` +
+
+

${this.title}

+
${this.__logCounter}
+ +
+
+
+
+
+
+ `; + } +} diff --git a/packages/helpers/sb-action-logger/stories/index.stories.mdx b/packages/helpers/sb-action-logger/stories/index.stories.mdx new file mode 100644 index 000000000..d1beeddfb --- /dev/null +++ b/packages/helpers/sb-action-logger/stories/index.stories.mdx @@ -0,0 +1,116 @@ +import { + Story, + Preview, + Meta, + Props, + html, +} from '@open-wc/demoing-storybook'; + +import '../../sb-action-logger.js'; + + + +# Storybook Action Logger + +A visual element to show action logs in Storybook demos `sb-action-logger` + + + {html` + +
To log: Hello, World!
+ + + `} +
+ +```html +
To log: Hello, World!
+ + +``` + +Note that you need some reference to your logger. Above example shows this by using a unique ID. + +## Features: + +- A public method `log` to log things to the action logger. +- Overridable `title` property. +- Clear button to clear logs +- A counter to count the total amount of logs + +## How to use + +### Installation + +```bash +npm i sb-action-logger +``` + +## API + + + +## Variations + +### Custom Title + + + {html` + + + `} + + + +## Rationale + +This component was created due to a need for a nice visual action logger in Storybook Docs mode. + +In docs mode, you do not have access to the action logger addon component, and it is nice to be able to show actions inline in your demos and documentation. + +### Opinionated + +I added quite a bit of styling on this component to make it look decent. +There are a bunch of styles that you can easily override to make it fit your design system which I believe should be enough in most cases. +If you need more control you can always: + +- Override the host styles, there's a few custom CSS props that you can override as well. +- Extend the component and apply your overrides +- Open an issue if you believe it would be good to make something more flexible / configurable + +Maybe in the future I will abstract this component to a more generic (ugly) one with no styles, so it's more friendly as a shared component. + +### Plugin + +If you use an action logger inside your Story in Storybook, you will also see it in your canvas, and this may not be your intention. + +One idea I have is that we can simplify the usage further by making this a Storybook (docs-)plugin or decorator or whatever. +I am not too familiar with them at the moment, but it would be cool if someone can simply enable an action logger option on a particular Story inside their .mdx, +and then actions are automatically logged to the visual logger below it. Would need to figure out how to catch the action and pass it to the visual logger element. + +I have not investigated yet on the how, but that is the rough idea. Feel free to help out here :) + +## Future + +I plan on adding more features. +They can always be found in the test folder where I specify new features as tests first, and then I skip them until I implement them. Easy to find them that way. +If the feature you'd like is not in the tests, I probably did not think about it yet or did not plan to do it yet, so in that case feel free to make an issue so we can add it. + +I'm happy to accept pull requests for skipped tests (features to be added), see the CONTRIBUTING.md on GitHub for more details on how to contribute to this codebase. diff --git a/packages/helpers/sb-action-logger/test/sb-action-logger.test.js b/packages/helpers/sb-action-logger/test/sb-action-logger.test.js new file mode 100644 index 000000000..80a7a45e8 --- /dev/null +++ b/packages/helpers/sb-action-logger/test/sb-action-logger.test.js @@ -0,0 +1,125 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import '../../sb-action-logger.js'; + +// Note: skips are left out of first iteration + +describe('sb-action-logger', () => { + it('has a default title "Action Logger"', async () => { + const el = await fixture(html` + + `); + + expect(el.shadowRoot.querySelector('.header__title').innerText).to.equal('Action Logger'); + }); + + it('has a title property / attribute that can be overridden', async () => { + const el = await fixture(html` + + `); + + const titleEl = el.shadowRoot.querySelector('.header__title'); + + expect(titleEl.innerText).to.equal('Logging your favorite fruit'); + }); + + describe('Logger behavior', () => { + it('is possible to send something to the logger using the log method', async () => { + const el = await fixture(html` + + `); + + el.log('Hello, World!'); + + const loggerEl = el.shadowRoot.querySelector('.logger'); + + expect(loggerEl.children.length).to.equal(1); + expect(loggerEl.firstElementChild.innerText).to.equal('Hello, World!'); + }); + + it('appends new logs to the logger', async () => { + const el = await fixture(html` + + `); + + el.log('Hello, World!'); + el.log('Hello, Planet!'); + el.log('Hello, Earth!'); + el.log('Hello, World!'); + el.log('Hello, Planet!'); + + const loggerEl = el.shadowRoot.querySelector('.logger'); + + expect(loggerEl.children.length).to.equal(5); + }); + + it('shows a visual cue whenever something is logged to the logger', async () => { + const el = await fixture(html` + + `); + + const cueEl = el.shadowRoot.querySelector('.header__log-cue-overlay'); + expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.false; + + el.log('Hello, World!'); + expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.true; + }); + + it('has a visual counter that counts the amount of total logs', async () => { + const el = await fixture(html` + + `); + + const cueEl = el.shadowRoot.querySelector('.header__log-cue-overlay'); + + expect(cueEl.classList.contains('.header__log-cue-overlay--slide')).to.be.false; + + el.log('Hello, World!'); + expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.true; + }); + + it('has a clear button that clears the logs and resets the counter', async () => { + const el = await fixture(html` + + `); + + el.log('Hello, World!'); + el.log('Hello, Planet!'); + + const clearBtn = el.shadowRoot.querySelector('.header__clear'); + clearBtn.click(); + + expect(el.shadowRoot.querySelector('.logger').children.length).to.equal(0); + }); + + it('duplicate consecutive logs are kept as one', async () => { + const el = await fixture(html` + + `); + expect(el).to.be.true; + }); + }); + + describe('Potential Additional Features', () => { + it.skip('duplicate consecutive adds a visual counter to count per duplicate', async () => { + const el = await fixture(html` + + `); + expect(el).to.be.true; + }); + + // This is handy if you don't want to keep track of updates + it.skip('can be set to mode=simple for only showing a single log statement', async () => { + const el = await fixture(html` + + `); + expect(el).to.be.true; + }); + + it.skip('fires a sb-action-logged event when something is logged to the logger', async () => { + const el = await fixture(html` + + `); + expect(el).to.be.true; + }); + }); +}); diff --git a/packages/helpers/sb-locale-switcher.js b/packages/helpers/sb-locale-switcher.js new file mode 100644 index 000000000..b69598f0c --- /dev/null +++ b/packages/helpers/sb-locale-switcher.js @@ -0,0 +1,3 @@ +import { SbLocaleSwitcher } from './sb-locale-switcher/src/SbLocaleSwitcher.js'; + +window.customElements.define('sb-locale-switcher', SbLocaleSwitcher); diff --git a/packages/helpers/sb-locale-switcher/src/SbLocaleSwitcher.js b/packages/helpers/sb-locale-switcher/src/SbLocaleSwitcher.js new file mode 100644 index 000000000..09857fc87 --- /dev/null +++ b/packages/helpers/sb-locale-switcher/src/SbLocaleSwitcher.js @@ -0,0 +1,31 @@ +import { LitElement, html } from '@lion/core'; + +export class SbLocaleSwitcher extends LitElement { + static get properties() { + return { + showLocales: { type: Array, attribute: 'show-locales' }, + }; + } + + constructor() { + super(); + this.showLocales = ['en-GB', 'en-US', 'en-AU', 'nl-NL', 'nl-BE']; + } + + // eslint-disable-next-line class-methods-use-this + callback(locale) { + document.documentElement.lang = locale; + } + + render() { + return html` + ${this.showLocales.map( + showLocale => html` + + `, + )} + `; + } +}