diff --git a/ui/.storybook/preview.js b/ui/.storybook/preview.js index 8aa5d32..aaf4d8c 100644 --- a/ui/.storybook/preview.js +++ b/ui/.storybook/preview.js @@ -3,7 +3,7 @@ const preview = { parameters: { options: { storySort: { - order: ['Web UI Storybook', 'Design System', 'Layout','Forms', 'Components', 'Utilities', 'Pages'], + order: ['Web UI Storybook', 'Design System', 'Layout','Forms', 'Components', 'Web Components Or Custom Elements', 'Utilities', 'Pages'], }, }, }, diff --git a/ui/package.json b/ui/package.json index 690c7bf..13822cd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -2,7 +2,7 @@ "name": "web-ui-boilerplate", "description": "UI boilerplate for websites/webapps using vanilla HTML/CSS/JavaScript, powered by Storybook, bundled by Parcel.", "author": "basher", - "version": "2.0.0", + "version": "2.1.0", "license": "ISC", "repository": { "type": "git", diff --git a/ui/src/javascript/modules/disclosure.ts b/ui/src/javascript/modules/disclosure.ts index 4c35b52..8a244a5 100644 --- a/ui/src/javascript/modules/disclosure.ts +++ b/ui/src/javascript/modules/disclosure.ts @@ -5,6 +5,8 @@ export default class Disclosure { private disclosure: Element; private btnDisclosure: HTMLButtonElement | null; private content: HTMLElement | null; + private bindEscapeKey?: boolean; + private bindClickOutside?: boolean; constructor(disclosure: Element) { this.disclosure = disclosure; @@ -14,6 +16,12 @@ export default class Disclosure { this.content = this.disclosure.querySelector( '[data-disclosure-content]', ); + this.bindEscapeKey = this.disclosure.hasAttribute( + 'data-disclosure-escape-key', + ); + this.bindClickOutside = this.disclosure.hasAttribute( + 'data-disclosure-click-outside', + ); this.init(); } @@ -35,20 +43,18 @@ export default class Disclosure { } private initdisclosure(): void { + const button = this.btnDisclosure; + const content = this.content; + const bindEscapeKey = this.bindEscapeKey; + const bindClickOutside = this.bindClickOutside; + // Show/hide content. - const button = this.btnDisclosure as HTMLElement; - const content = this.content as HTMLElement; - const bindEscapeKey = this.disclosure.hasAttribute( - 'data-disclosure-escape-key', - ); - const bindClickOutside = this.disclosure.hasAttribute( - 'data-disclosure-click-outside', - ); - disclosure({ - button, - content, - bindEscapeKey, - bindClickOutside, - }); + button && + disclosure({ + button, + content, + bindEscapeKey, + bindClickOutside, + }); } } diff --git a/ui/src/javascript/ui-init.ts b/ui/src/javascript/ui-init.ts index e254f82..f97d922 100644 --- a/ui/src/javascript/ui-init.ts +++ b/ui/src/javascript/ui-init.ts @@ -13,6 +13,8 @@ import Tabs from './modules/tabs'; import Toggle from './modules/toggle'; import VideoPlayer from './modules/video-player'; +import WebDisclosure from './web-components/web-disclosure'; + // For DEMO purposes only. import demoAjaxFetchHTML from './modules/demo-ajax-fetch-html'; @@ -33,4 +35,8 @@ export const uiInit = (): void => { // For DEMO purposes only. demoAjaxFetchHTML.start(); + + // Define Web Components + !customElements.get('web-disclosure') && + customElements.define('web-disclosure', WebDisclosure); }; diff --git a/ui/src/javascript/utils/disclosure.ts b/ui/src/javascript/utils/disclosure.ts index 80572d0..1b7d55b 100644 --- a/ui/src/javascript/utils/disclosure.ts +++ b/ui/src/javascript/utils/disclosure.ts @@ -14,7 +14,7 @@ import { randomString } from './random-string'; * disclosure({ button, content }); */ interface Disclosure { - button: HTMLElement; + button: HTMLButtonElement; content: HTMLElement | null; bindEscapeKey?: boolean; bindClickOutside?: boolean; diff --git a/ui/src/javascript/web-components/web-disclosure.ts b/ui/src/javascript/web-components/web-disclosure.ts new file mode 100644 index 0000000..fdfa69e --- /dev/null +++ b/ui/src/javascript/web-components/web-disclosure.ts @@ -0,0 +1,37 @@ +import { disclosure } from '../utils/disclosure'; + +export default class WebDisclosure extends HTMLElement { + private trigger: HTMLButtonElement | null; + private content: HTMLElement | null; + private bindEscapeKey?: boolean; + private bindClickOutside?: boolean; + + constructor() { + super(); + + this.trigger = this.querySelector('[trigger]'); + this.content = this.querySelector('[content]'); + this.bindEscapeKey = this.hasAttribute('bind-escape-key'); + this.bindClickOutside = this.hasAttribute('bind-click-outside'); + + this.init(); + + // NOTE: There are NO event listeners here. All events are handled by the external 'discloure()' dependency. + } + + private init(): void { + if (!this.trigger || !this.content) return; + + const button = this.trigger; + const content = this.content; + const bindEscapeKey = this.bindEscapeKey; + const bindClickOutside = this.bindClickOutside; + + disclosure({ + button, + content, + bindEscapeKey, + bindClickOutside, + }); + } +} diff --git a/ui/src/stylesheets/index.scss b/ui/src/stylesheets/index.scss index e5e7a1f..b91261a 100644 --- a/ui/src/stylesheets/index.scss +++ b/ui/src/stylesheets/index.scss @@ -45,6 +45,7 @@ CSS Cascade Layers. @layer components { @include meta.load-css('form'); @include meta.load-css('components'); + @include meta.load-css('web-components'); } /* diff --git a/ui/src/stylesheets/web-components/_index.scss b/ui/src/stylesheets/web-components/_index.scss new file mode 100644 index 0000000..b8d7954 --- /dev/null +++ b/ui/src/stylesheets/web-components/_index.scss @@ -0,0 +1 @@ +@use 'web-disclosure'; diff --git a/ui/src/stylesheets/web-components/_web-disclosure.scss b/ui/src/stylesheets/web-components/_web-disclosure.scss new file mode 100644 index 0000000..19da0d3 --- /dev/null +++ b/ui/src/stylesheets/web-components/_web-disclosure.scss @@ -0,0 +1,11 @@ +/* +---------------------------------------------------------------------------- +Dependencies. +---------------------------------------------------------------------------- +*/ +// @use '../base' as *; +// @use '../mixins' as *; + +web-disclosure { + // +} diff --git a/ui/stories/3. Layout/UsefulTips.mdx b/ui/stories/3. Layout/UsefulTips.mdx index 2babb03..e72e45d 100644 --- a/ui/stories/3. Layout/UsefulTips.mdx +++ b/ui/stories/3. Layout/UsefulTips.mdx @@ -1,6 +1,6 @@ import { Meta } from '@storybook/blocks'; - + # Useful layout tips diff --git a/ui/stories/5. Components/Disclosure/Disclosure.mdx b/ui/stories/5. Components/Disclosure/Disclosure.mdx index 324f5f2..30e1370 100644 --- a/ui/stories/5. Components/Disclosure/Disclosure.mdx +++ b/ui/stories/5. Components/Disclosure/Disclosure.mdx @@ -12,5 +12,8 @@ import * as Disclosure from './Disclosure.stories'; - By adding the `data-disclosure-escape-key` attribute to the HTML, the `ESC` key can be used to hide the content. - Similarly, the `data-disclosure-click-outside` attribute hides the content when clicking anywhere outside the content. +## Web component version +- See the [web-disclosure](/story/web-components-web-disclosure--web-disclosure) component. + diff --git a/ui/stories/5. Components/Disclosure/Disclosure.stories.js b/ui/stories/5. Components/Disclosure/Disclosure.stories.js index b155da5..b7d8eb7 100644 --- a/ui/stories/5. Components/Disclosure/Disclosure.stories.js +++ b/ui/stories/5. Components/Disclosure/Disclosure.stories.js @@ -1,7 +1,7 @@ import { DisclosureHtml } from './Disclosure'; export default { - title: 'Components/Disclosure', + title: 'Components/Disclosure (Or Show|Hide)', parameters: { status: { type: 'stable', @@ -22,4 +22,3 @@ export default { export const Disclosure = { render: (args) => DisclosureHtml(args), }; -Disclosure.storyName = 'Disclosure (i.e. Show/Hide)'; diff --git a/ui/stories/6. Web Components Or Custom Elements/WebComponents.mdx b/ui/stories/6. Web Components Or Custom Elements/WebComponents.mdx new file mode 100644 index 0000000..998086d --- /dev/null +++ b/ui/stories/6. Web Components Or Custom Elements/WebComponents.mdx @@ -0,0 +1,13 @@ +import { Meta } from '@storybook/blocks'; + + + +# HTML Web Components +- The Web Components in this boilerplate are simply `custom elements` that wrap normal HTML markup, therefore providing a progressive enhancement layer via JavaScript. +- They are **not** empty shells that only work with JavaScript. +- They also don't make use of the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). + +## Additional reading +- Great explanation by Jeremy Keith (and others) about the power of [HTML Web Components](https://adactio.com/journal/20618). +- Simple example from Chris Ferdinandi showing how to [create a Web Component from scratch](https://gomakethings.com/lets-create-a-web-component-from-scratch/). He also has lots more articles on this topic in his blog. +- Some notes on the recommended use of [connectedCallback() lifecycle method and event listeners](https://hawkticehurst.com/writing/you-are-probably-using-connectedcallback-wrong/). diff --git a/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.js b/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.js new file mode 100644 index 0000000..c1bbe23 --- /dev/null +++ b/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.js @@ -0,0 +1,21 @@ +export const WebDisclosureHtml = (args) => ` + + + Show / Hide + + + + Content to be shown/hidden. + Use this component when accordion or tabs components cannot be used. + + +`; diff --git a/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.mdx b/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.mdx new file mode 100644 index 0000000..8ef8b5a --- /dev/null +++ b/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.mdx @@ -0,0 +1,12 @@ +import { Meta, Canvas, Controls } from '@storybook/blocks'; +import * as WebDisclosure from './WebDisclosure.stories'; + + + +# `` +- This is functionally equivalent to the [disclosure](/story/components-disclosure--disclosure) component, with the same accessibility considerations. +- The attributes that bind clicking the `ESC` key and clicking outside are renamed to `bind-escape-key` and `bind-click-outside`. +- To maintain the [DRY code](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) approach in this boilerplate, the JavaScript logic to handle showing/hiding of content is imported from an external shared utility function in `javascript/utils/disclosure.ts`. + + + diff --git a/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.stories.js b/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.stories.js new file mode 100644 index 0000000..8349b21 --- /dev/null +++ b/ui/stories/6. Web Components Or Custom Elements/WebDisclosure/WebDisclosure.stories.js @@ -0,0 +1,25 @@ +import { WebDisclosureHtml } from './WebDisclosure'; + +export default { + title: 'Web Components Or Custom Elements/', + parameters: { + status: { + type: 'stable', + }, + }, + argTypes: { + bindEscapeKey: { + control: 'boolean', + description: 'Close with ESC key.' + }, + bindClickOutside: { + control: 'boolean', + description: 'Close by clicking outside' + }, + }, +}; + +export const WebDisclosure = { + render: (args) => WebDisclosureHtml(args), +}; +WebDisclosure.storyName = ''; diff --git a/ui/stories/6. Utilities/Utilities.js b/ui/stories/7. Utilities/Utilities.js similarity index 100% rename from ui/stories/6. Utilities/Utilities.js rename to ui/stories/7. Utilities/Utilities.js diff --git a/ui/stories/6. Utilities/Utilities.mdx b/ui/stories/7. Utilities/Utilities.mdx similarity index 100% rename from ui/stories/6. Utilities/Utilities.mdx rename to ui/stories/7. Utilities/Utilities.mdx diff --git a/ui/stories/6. Utilities/Utilities.stories.js b/ui/stories/7. Utilities/Utilities.stories.js similarity index 95% rename from ui/stories/6. Utilities/Utilities.stories.js rename to ui/stories/7. Utilities/Utilities.stories.js index 9fdeb16..200ef38 100644 --- a/ui/stories/6. Utilities/Utilities.stories.js +++ b/ui/stories/7. Utilities/Utilities.stories.js @@ -1,7 +1,7 @@ import { MarginPaddingHtml } from './Utilities'; export default { - title: 'Utilities/Helpers and utilities', + title: 'Utilities/Helpers and Utilities', parameters: { status: { type: 'stable', diff --git a/ui/stories/6. Utilities/UtilitiesNoArgs.stories.js b/ui/stories/7. Utilities/UtilitiesNoArgs.stories.js similarity index 91% rename from ui/stories/6. Utilities/UtilitiesNoArgs.stories.js rename to ui/stories/7. Utilities/UtilitiesNoArgs.stories.js index aa942e8..d3a7350 100644 --- a/ui/stories/6. Utilities/UtilitiesNoArgs.stories.js +++ b/ui/stories/7. Utilities/UtilitiesNoArgs.stories.js @@ -5,7 +5,7 @@ import { } from './Utilities'; export default { - title: 'Utilities/Helpers and utilities', + title: 'Utilities/Helpers and Utilities', parameters: { status: { type: 'stable', diff --git a/ui/stories/7. Pages/Homepage.js b/ui/stories/8. Pages/Homepage.js similarity index 100% rename from ui/stories/7. Pages/Homepage.js rename to ui/stories/8. Pages/Homepage.js diff --git a/ui/stories/7. Pages/Homepage.stories.js b/ui/stories/8. Pages/Homepage.stories.js similarity index 100% rename from ui/stories/7. Pages/Homepage.stories.js rename to ui/stories/8. Pages/Homepage.stories.js
Content to be shown/hidden.
Use this component when accordion or tabs components cannot be used.
accordion
tabs