Skip to content

Commit

Permalink
Merge pull request #26 from hlxsites/icon-active-state
Browse files Browse the repository at this point in the history
Add Icon active state
  • Loading branch information
infloent authored Jun 7, 2024
2 parents ad729cd + 1004321 commit b96fac2
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 42 deletions.
102 changes: 73 additions & 29 deletions blocks/icon/icon.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import ComponentBase from '../../scripts/component-base.js';
import { stringToJsVal } from '../../scripts/libs.js';

export default class Icon extends ComponentBase {
static observedAttributes = ['data-icon'];
static observedAttributes = ['data-active', 'data-icon'];

nestedComponentsConfig = {};
#initialIcon = null;

#activeIcon = null;

get cache() {
window.ICONS_CACHE ??= {};
return window.ICONS_CACHE;
}

get isActive() {
return stringToJsVal(this.dataset.active) === true;
}

extendConfig() {
return [
Expand All @@ -28,59 +40,92 @@ export default class Icon extends ComponentBase {
}
}

get iconUrl() {
return `assets/icons/${this.iconName}.svg`;
setDefaults() {
super.setDefaults();
this.nestedComponentsConfig = {};
}

get cache() {
window.ICONS_CACHE = window.ICONS_CACHE || {};
return window.ICONS_CACHE;
iconUrl(iconName) {
return `assets/icons/${iconName}.svg`;
}

async connected() {
this.setAttribute('aria-hidden', 'true');
}

onAttributeIconChanged({ oldValue, newValue }) {
// Same icon component can be reused with any other icons just by changing the attribute
async onAttributeIconChanged({ oldValue, newValue }) {
if (oldValue === newValue) return;

// ! The initial and active icon names are separated with a double underline
// ! The active icon is optional;
const [initial, active] = newValue.split('__');
this.#initialIcon = initial;
this.#activeIcon = active || null;

// Start loading both icons;
const loadInitialIcon = this.loadIcon(this.#initialIcon);
const loadActiveIcon = this.#activeIcon ? this.loadIcon(this.#activeIcon) : null;

const isActiveWithIcon = this.isActive && this.#activeIcon;
// Wait only for the current icon
if (isActiveWithIcon) {
await loadActiveIcon;
} else {
await loadInitialIcon;
}
this.displayIcon(isActiveWithIcon ? this.#activeIcon : this.#initialIcon);
}

// If there is an active icon toggle the icons
async onAttributeActiveChanged({ oldValue, newValue }) {
if (!this.initialized) return;
if (oldValue === newValue) return;
this.loadIcon(newValue);
if (!this.#activeIcon) return;
this.displayIcon(this.isActive ? this.#activeIcon : this.#initialIcon);
}

async loadIcon(icon) {
this.iconName = icon;
if (!this.cache[this.iconName]) {
this.cache[this.iconName] = {
loading: new Promise((resolve) => {
resolve(this.loadFragment(this.iconUrl));
}),
displayIcon(iconName) {
this.innerHTML = this.template(iconName);
}

// Load icon can be used externally to load additional icons in the cache
async loadIcon(iconName) {
// this.iconName = icon;
if (!this.cache[iconName]) {
this.cache[iconName] = {
loading: this.loadFragment(this.iconUrl(iconName), iconName),
};
} else {
await this.cache[this.iconName].loading;
this.innerHTML = this.template();
}
this.classList.add('loaded');
await this.cache[iconName].loading;
}

template() {
const { viewBox } = this.cache[this.iconName];
template(iconName) {
if (!this.cache[iconName]) return '';
const { viewBox } = this.cache[iconName];
const attributes = Object.keys({ viewBox })
.map((k) => {
if (this.cache[this.iconName][k]) {
return `${k}="${this.cache[this.iconName][k]}"`;
if (this.cache[iconName][k]) {
return `${k}="${this.cache[iconName][k]}"`;
}
return '';
})
.join(' ');
return `<svg focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ${attributes}><use xlink:href="#icons-sprite-${this.iconName}"/></svg>`;
return `<svg focusable="false" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ${attributes}><use xlink:href="#icons-sprite-${iconName}"/></svg>`;
}

iconTemplate(iconName, svg, viewBox, width, height) {
return `<defs><g id="icons-sprite-${iconName}" viewBox="${viewBox}" width="${width}" height="${height}">${svg.innerHTML}</g></defs>`;
}

async processFragment(response) {
async loadFragment(path, iconName) {
if (typeof path !== 'string') return;
const response = await this.getFragment(path);
await this.processFragment(response, iconName);
}

async processFragment(response, iconName) {
if (response.ok) {
const { iconName } = this;
this.svg = await response.text();

if (this.svg.match(/(<style | class=|url\(#| xlink:href="#)/)) {
Expand All @@ -106,9 +151,8 @@ export default class Icon extends ComponentBase {
this.cache[iconName].svg = svg;
}
this.svgSprite.append(this.cache[iconName].svg);
this.innerHTML = this.template();
} else {
this.cache[this.iconName] = false;
this.cache[iconName] = false;
}
}
}
2 changes: 1 addition & 1 deletion blocks/navigation/navigation.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ raqn-navigation.active > nav > div {
display: flex;
}

raqn-navigation > nav button {
raqn-navigation > button {
display: inline-flex;
justify-self: end;
align-items: center;
Expand Down
33 changes: 21 additions & 12 deletions blocks/navigation/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@ export default class Navigation extends ComponentBase {
targetsSelectors: ':scope > :is(:first-child)',
};

async ready() {
dependencies = ['icon'];

setDefaults() {
super.setDefaults();
this.active = {};
this.isActive = false;
this.navContentInit = false;
this.navCompactedContentInit = false;
}

async ready() {
this.navContent = this.querySelector('ul');
this.innerHTML = '';
this.navContentInit = false;
this.navCompactedContent = this.navContent.cloneNode(true); // the clone need to be done before `this.navContent` is modified
this.navCompactedContentInit = false;
this.nav = document.createElement('nav');
this.append(this.nav);
this.setAttribute('role', 'navigation');

this.dataset.icon ??= 'menu';

this.isCompact = this.dataset.compact === 'true';
this.dataset.icon ??= 'menu';
this.append(this.nav);
this.nav.setAttribute('role', 'navigation');
this.nav.setAttribute('id', 'navigation');

if (this.isCompact) {
await this.setupCompactedNav();
Expand All @@ -36,7 +42,7 @@ export default class Navigation extends ComponentBase {
this.navContentInit = true;
this.setupClasses(this.navContent);
}

this.navButton?.remove();
this.nav.append(this.navContent);
}

Expand All @@ -48,12 +54,13 @@ export default class Navigation extends ComponentBase {
this.navCompactedContent.addEventListener('click', (e) => this.activate(e));
}

this.nav.append(this.createButton());
this.prepend(this.createButton());
this.nav.append(this.navCompactedContent);
}

onAttributeCompactChanged({ newValue }) {
onAttributeCompactChanged({ oldValue, newValue }) {
if (!this.initialized) return;
if (oldValue === newValue) return;
this.isCompact = newValue === 'true';
this.nav.innerHTML = '';

Expand Down Expand Up @@ -82,8 +89,10 @@ export default class Navigation extends ComponentBase {
this.navButton.innerHTML = `<raqn-icon data-icon=${this.dataset.icon}></raqn-icon>`;
this.navIcon = this.navButton.querySelector('raqn-icon');
this.navButton.addEventListener('click', () => {
this.isActive = !this.isActive;
this.classList.toggle('active');
this.navButton.setAttribute('aria-expanded', this.classList.contains('active'));
this.navButton.setAttribute('aria-expanded', this.isActive);
this.navIcon.dataset.active = this.isActive;
});
return this.navButton;
}
Expand Down

0 comments on commit b96fac2

Please sign in to comment.