vanilla-hamburger
Version:
A tiny framework agnostic hamburger button element for modern web apps
211 lines • 6.58 kB
JavaScript
var _a;
import { createTemplate, createRoot } from '../utils/dom.js';
import { props, render, update } from '../internals.js';
import css from '../styles/burger.js';
const AREA = 48;
const tpl = createTemplate(`<style>${css}</style>`);
const btn = Symbol('btn');
const updating = Symbol('updating');
const prepare = Symbol('prepare');
export const defaultProps = {
size: 32,
direction: 'left',
disabled: false,
distance: 'md',
duration: 0.4,
label: 'hamburger',
easing: 'cubic-bezier(0, 0, 0, 1)',
pressed: false
};
export class Burger extends HTMLElement {
constructor() {
super();
this[_a] = {};
createRoot(this, tpl);
this.addEventListener('click', this);
}
static get observedAttributes() {
return ['disabled', 'distance', 'duration', 'easing', 'pressed', 'size', 'label'];
}
/**
* A valid `transition-timing-function` CSS value, for example 'ease-out'.
* @type {string}
* @default cubic-bezier(0, 0, 0, 1)
*/
get easing() {
return this[props].easing;
}
set easing(easing) {
this[props].easing = easing;
this[update]();
}
/**
* The vertical distance between the lines. Small (sm), medium (md) or large (lg).
* @type {'sm' | 'md' | 'lg'}
* @default md
*/
get distance() {
return this[props].distance;
}
set distance(distance) {
this[props].distance = distance;
this[update]();
}
/**
* The duration of the animation. Can be set to zero if no animation is desired.
* @type {number}
* @default 0.4
*/
get duration() {
return this[props].duration;
}
set duration(duration) {
this[props].duration = duration;
this[update]();
}
/**
* When set to true, the internal <button> element is disabled.
* @type {boolean}
* @attr disabled
* @default false
*/
get disabled() {
return this[props].disabled;
}
set disabled(disabled) {
this[props].disabled = disabled;
this.toggleAttribute('disabled', disabled);
this[btn] && this[btn].toggleAttribute('disabled', disabled);
}
/**
* Accessible label set on the internal <button> element for screen readers.
* @type {string}
* @default {hamburger}
*/
get label() {
return this[props].label;
}
set label(label) {
this[props].label = label;
this[btn] && this[btn].setAttribute('aria-label', label);
}
/**
* Set to true when element is pressed.
* @type {boolean}
* @attr pressed
* @default false
*/
get pressed() {
return this[props].pressed;
}
set pressed(pressed) {
this[props].pressed = pressed;
this.toggleAttribute('pressed', !!pressed);
this[btn] && this[btn].setAttribute('aria-pressed', `${!!pressed}`);
this[update]();
}
/**
* Size of the icon. Should be a number between 12 and 48.
* @type {number}
* @default 32
*/
get size() {
return this[props].size;
}
set size(size) {
this[props].size = size;
this[update]();
}
connectedCallback() {
this[btn] = this.shadowRoot.querySelector('button');
this.constructor.observedAttributes.forEach((k) => {
// A user may set a property on an _instance_ of an element,
// before its prototype has been connected to this class.
// If so, we need to run it through the proper class setter.
if (this.hasOwnProperty(k)) {
const value = this[k];
delete this[k];
this[k] = value;
}
else if (!this[k]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this[k] = defaultProps[k];
}
});
}
attributeChangedCallback(prop, oldVal, newVal) {
if (oldVal !== newVal) {
let value = newVal;
if (prop == 'size' || prop == 'duration') {
value = newVal === null ? null : Number(newVal);
}
else if (prop == 'pressed' || prop == 'disabled') {
value = newVal !== null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this[prop] = value;
}
}
handleEvent(event) {
if (event.type === 'click') {
this.pressed = !this.pressed;
this.dispatchEvent(new CustomEvent('pressed-changed', { detail: { value: this.pressed } }));
}
}
focus() {
this[btn] && this[btn].focus();
}
blur() {
this[btn] && this[btn].blur();
}
click() {
if (!this.disabled) {
super.click();
}
}
[(_a = props, prepare)]() {
const { distance, lines } = this;
const width = Math.max(12, Math.min(AREA, this.size));
const barHeightRaw = width / 12;
const barHeight = Math.round(barHeightRaw);
const space = distance === 'lg' ? 0.25 : distance === 'sm' ? 0.75 : 0.5;
const marginRaw = width / (lines * (space + (lines === 3 ? 1 : 1.25)));
const margin = Math.round(marginRaw);
const height = barHeight * lines + margin * (lines - 1);
const translate = lines === 3
? distance === 'lg'
? 4.0425
: distance === 'sm'
? 5.1625
: 4.6325
: distance === 'lg'
? 6.7875
: distance === 'sm'
? 8.4875
: 7.6675;
const deviation = (barHeightRaw - barHeight + (marginRaw - margin)) / (lines === 3 ? 1 : 2);
const barStyles = {
height: `${barHeight}px`,
left: `${Math.round((AREA - width) / 2)}px`,
width: `${width}px`
};
return {
barHeight,
barStyles,
margin,
move: parseFloat((width / translate - deviation / (4 / 3)).toFixed(2)),
pressed: this.pressed,
time: Math.max(0, this.duration),
topOffset: Math.round((AREA - height) / 2)
};
}
async [update]() {
if (!this[updating]) {
this[updating] = true;
// Ensure that property changes are batched.
this[updating] = await false;
this[render](this[prepare]());
}
}
}
//# sourceMappingURL=burger.js.map