diff --git a/blocks/accordion/accordion.css b/blocks/accordion/accordion.css new file mode 100644 index 00000000..1d82f20c --- /dev/null +++ b/blocks/accordion/accordion.css @@ -0,0 +1,79 @@ +raqn-accordion { + --scope-icon-size: 1em; + --accordion-background-color: var(--scope-background, black); + --accordion-color: var(--scope-color, white); + + background-color: var(--accordion-background-color); + color: var(--accordion-color); + margin: var(--scope-margin, 0); + display: grid; + + & .accordion-control { + border-block-start: var(--scope-border-block-start, none); + border-inline-start: var(--scope-border-inline-start, none); + border-inline-end: var(--scope-border-inline-end, none); + + &:first-child { + border-block-start: none + } + + & > * { + --scope-headings-color: var(--scope-color, black); + --scope-hover-color: var(--scope-accent-color, gray); + + width: 100%; + display: flex; + justify-content: space-between; + min-width: 100%; + } + + cursor: pointer; + display: flex; + align-items: center; + justify-content: start; + width: 100%; + + &:hover { + --scope-color: var(--scope-headings-color); + } + } + + & raqn-icon { + align-self: end; + transform: rotate(90deg); + transition: transform 0.2s ease-in-out; + } + + & accordion-control.active raqn-icon { + transform: rotate(270deg); + } + + & .accordion-content { + display: grid; + max-height: 0; + overflow: hidden; + opacity: 0; + border-block-end: var(--scope-border-block-end, none); + border-block-start: var(--scope-border-block-start, none); + margin-block-end: -1px; + transition: max-height 0.5s ease-in-out, + opacity 0.5s ease-in-out; + + + &:last-child { + border-block-end: none + } + + &.active { + opacity: 1; + grid-template-rows: 1fr; + max-height: 100vw; + + } + } + + & .accordion-content-wrapper { + margin-block: 1em; + display: grid; + } +} \ No newline at end of file diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js new file mode 100644 index 00000000..7d090f4b --- /dev/null +++ b/blocks/accordion/accordion.js @@ -0,0 +1,70 @@ +import Column from '../column/column.js'; + +export default class Accordion extends Column { + ready() { + this.setAttribute('role', 'navigation'); + let children = Array.from(this.children); + children = children.map((child) => { + if (child.tagName !== 'DIV') { + const div = document.createElement('div'); + div.append(child); + this.append(div); + return div; + } + return child; + }); + // console.log(children) + this.setupControls(children.filter((_, ind) => ind % 2 === 0)); + this.setupContent(children.filter((_, ind) => ind % 2 === 1)); + } + + setupControls(controls) { + controls.forEach((control,index) => { + const icon = document.createElement('raqn-icon'); + icon.setAttribute('icon', 'chevron-right'); + const children = Array.from(control.children); + if (children.length === 0) { + const child = document.createElement('span'); + child.textContent = control.textContent; + control.innerHTML = ''; + control.append(child); + } + control.children[0].append(icon); + control.setAttribute('role', 'button'); + control.setAttribute('aria-expanded', 'false'); + control.setAttribute('tabindex', '0'); + control.classList.add('accordion-control'); + control.id = `accordion-${this.id}-${index}`; + control.addEventListener('click', () => this.toggleControl(control)); + control.addEventListener('keypress', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + this.toggleControl(control); + } + }); + }); + } + + toggleControl(control) { + const content = control.nextElementSibling; + if (content) { + content.classList.toggle('active'); + control.classList.toggle('active'); + control.setAttribute('aria-expanded', content.classList.contains('active')); + content.setAttribute('aria-hidden', !content.classList.contains('active')); + } + } + + setupContent(contents) { + contents.forEach((content) => { + const internal = content.children; + const wrapper = document.createElement('div'); + wrapper.classList.add('accordion-content-wrapper'); + wrapper.append(...internal); + content.append(wrapper); + content.setAttribute('role', 'region'); + content.setAttribute('aria-hidden',true); + content.classList.add('accordion-content'); + content.setAttribute('aria-labelledby', content.previousElementSibling.id); + }); + } +} \ No newline at end of file diff --git a/blocks/column/column.js b/blocks/column/column.js index b4a095c4..d4a72505 100644 --- a/blocks/column/column.js +++ b/blocks/column/column.js @@ -6,9 +6,16 @@ export default class Column extends ComponentBase { } connected() { - const firstChild = this.children[0]; - const content = this.querySelector('div > div'); - firstChild.replaceWith(...content.children); + const content = this.querySelectorAll('div > div'); + // clean up dom structure (div div div div div div) and save the content + this.contentChildren = Array.from(content).map((child) => { + const {children} = child; + const parent = child.parentNode; + if (children.length > 0) { + child.replaceWith(...children); + } + return parent; + }) this.calculateGridTemplateColumns(); } diff --git a/blocks/navigation/navigation.css b/blocks/navigation/navigation.css index 33106453..f89ae66b 100644 --- a/blocks/navigation/navigation.css +++ b/blocks/navigation/navigation.css @@ -16,6 +16,18 @@ raqn-navigation { & p { display: none; } + + & ul { + overflow-y: auto; + max-height: calc(100vh - var(--scope-header-height)); + } + } + + &.active > nav { + & ul, + & p { + display: block; + } } & a { @@ -50,7 +62,7 @@ raqn-navigation { } &.active { - button { + & button { background-color: var(--scope-background-hover, #000); color: var(--scope-color-hover, #fff); } @@ -65,6 +77,7 @@ raqn-navigation { inset-inline-start: 0; inset-block-start: var(--scope-header-height, 64px); min-height: 100%; + max-height: calc(100vh - var(--scope-header-height, 64px)); margin: 0 auto; padding: 0; @@ -76,8 +89,12 @@ raqn-navigation { } } - /* desktop */ - &:not([compact='true']) > nav { + & .accordion-content-wrapper { + margin: 0; + } + + +&:not([compact='true']) > nav { & a { line-height: var(--scope-icon-size, 24px); } @@ -104,6 +121,16 @@ raqn-navigation { padding: var(--padding-vertical, 20px) var(--padding-horizontal, 20px); } + & .level-2 > a { + color: var(--scope-link-color-hover); + font-size: 1.2em; + font-weight: bold; + + &:hover { + color: var(--scope-color, #fff); + } + } + & .level-2, & .level-2 > ul { display: inline-flex; diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index 0295a235..303d3683 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -1,6 +1,8 @@ +import { start } from '../../scripts/init.js'; import Column from '../column/column.js'; export default class Navigation extends Column { + createButton() { const button = document.createElement('button'); button.setAttribute('aria-label', 'Menu'); @@ -27,6 +29,7 @@ export default class Navigation extends Column { this.icon = this.getAttribute('icon') || 'menu'; if (this.compact) { this.nav.append(this.createButton()); + start({name:'accordion'}); } this.append(this.nav); this.setupClasses(this.list); @@ -35,17 +38,33 @@ export default class Navigation extends Column { } } + createIcon(name = this.icon) { + const icon = document.createElement('raqn-icon'); + icon.setAttribute('icon', name); + return icon; + } + + creaeteAccordion(replaceChildrenElement) { + const accordion = document.createElement('raqn-accordion'); + const children = Array.from(replaceChildrenElement.children); + accordion.append(...children); + replaceChildrenElement.append(accordion); + } + setupClasses(ul, level = 1) { const children = Array.from(ul.children); children.forEach((child) => { + const hasChildren = child.querySelector('ul'); child.classList.add(`level-${level}`); child.dataset.level = level; - const hasChildren = child.querySelector('ul'); + if (hasChildren) { const anchor = child.querySelector('a'); - const icon = document.createElement('raqn-icon'); - icon.setAttribute('icon', 'chevron-right'); - anchor.append(icon); + if (this.compact) { + this.creaeteAccordion(child); + } else if (level === 1) { + anchor.append(this.createIcon('chevron-right')); + } child.classList.add('has-children'); this.setupClasses(hasChildren, level + 1); } diff --git a/blocks/navigation/navigation.md b/blocks/navigation/navigation.md new file mode 100644 index 00000000..e69de29b diff --git a/scripts/component-base.js b/scripts/component-base.js index bab81dc0..dac88028 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -8,12 +8,16 @@ export default class ComponentBase extends HTMLElement { } async connectedCallback() { - this.setAttribute('id', this.uuid); - if (this.external) { - await this.load(this.external); + const inicialized = this.getAttribute('inicialized'); + if (!inicialized) { + this.setAttribute('inicialized', true); + this.setAttribute('id', this.uuid); + if (this.external) { + await this.load(this.external); + } + this.connected(); + this.ready(); } - this.connected(); - this.ready(); } async load(block) { @@ -28,11 +32,15 @@ export default class ComponentBase extends HTMLElement { if (response.ok) { const html = await response.text(); this.innerHTML = html; - return init(this); + return this.refresh(this); } return response; } + refresh(el = this) { + init(el); + } + connected() {} ready() {} diff --git a/scripts/init.js b/scripts/init.js index df817e84..c09a0c53 100644 --- a/scripts/init.js +++ b/scripts/init.js @@ -38,6 +38,12 @@ function lcpPriority() { window.raqnLCP = lcp ? lcp.split(',').map((name) => ({ name })) : []; } + +export async function start({ name, el }) { + const loader = new ComponentLoader(name, el); + return loader.decorate(); +}; + export async function init(node = document) { let blocks = Array.from(node.querySelectorAll('[class]:not([class^=style]')); @@ -54,17 +60,12 @@ export async function init(node = document) { const rest = data.filter( ({ name }) => !lcp.includes(name) && !delay.includes(name), ); - const start = ({ name, el }) => { - const loader = new ComponentLoader(name, el); - return loader.decorate(); - }; // start with lcp and priority Promise.all([ ...lcp.map(({ name, el }) => start({ name, el })), ...priority.map(({ name, el }) => start({ name, el })), ]); - // timeout for the rest to proper prioritize in case of stalled loading rest.map(({ name, el }) => setTimeout(() => start({ name, el }))); diff --git a/styles/styles.css b/styles/styles.css index d264bac6..f15cde86 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -42,7 +42,7 @@ time, mark, audio, video { font: inherit; font-size: 100%; vertical-align: baseline; - color: inherit; + color: var(--scope-color, inherit); } header { @@ -86,7 +86,6 @@ main > div > div > div { display: grid; grid-template-columns: var(--scope-grid-template-columns, 1fr); gap: var(--scope-gap, 20px); - padding: var(--scope-padding, 20px 0); align-items: center; justify-items: start; min-height: var(--scope-font-size, 1.2em); @@ -153,3 +152,12 @@ h6 { #franklin-svg-sprite { display: none; } + +/* component setup styles */ + +raqn-navigation { + & > ul, + & > button { + display: none; + } +} \ No newline at end of file