Skip to content

Commit

Permalink
refactor: extract accordion and accordion-panel mixins
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan committed Dec 15, 2023
1 parent f349a02 commit 7d4cabc
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 267 deletions.
1 change: 1 addition & 0 deletions packages/accordion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"polymer"
],
"dependencies": {
"@open-wc/dedupe-mixin": "^1.3.0",
"@polymer/polymer": "^3.0.0",
"@vaadin/a11y-base": "24.4.0-alpha0",
"@vaadin/component-base": "24.4.0-alpha0",
Expand Down
30 changes: 30 additions & 0 deletions packages/accordion/src/vaadin-accordion-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @license
* Copyright (c) 2019 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';
import type { KeyboardDirectionMixinClass } from '@vaadin/a11y-base/src/keyboard-direction-mixin.js';

/**
* A mixin providing common accordion functionality.
*/
export declare function AccordionMixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<AccordionMixinClass> & Constructor<KeyboardDirectionMixinClass> & T;

export declare class AccordionMixinClass {
/**
* The index of currently opened panel. First panel is opened by
* default. Only one panel can be opened at the same time.
* Setting null or undefined closes all the accordion panels.
*/
opened: number | null;

/**
* The list of `<vaadin-accordion-panel>` child elements.
* It is populated from the elements passed to the light DOM,
* and updated dynamically when adding or removing panels.
*/
readonly items: HTMLElement[];
}
155 changes: 155 additions & 0 deletions packages/accordion/src/vaadin-accordion-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* @license
* Copyright (c) 2019 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { isElementFocused } from '@vaadin/a11y-base/src/focus-utils.js';
import { KeyboardDirectionMixin } from '@vaadin/a11y-base/src/keyboard-direction-mixin.js';
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';

/**
* A mixin providing common accordion functionality.
*
* @polymerMixin
* @mixes KeyboardDirectionMixin
*/
export const AccordionMixin = (superClass) =>
class AccordionMixinClass extends KeyboardDirectionMixin(superClass) {
static get properties() {
return {
/**
* The index of currently opened panel. First panel is opened by
* default. Only one panel can be opened at the same time.
* Setting null or undefined closes all the accordion panels.
* @type {number}
*/
opened: {
type: Number,
value: 0,
notify: true,
reflectToAttribute: true,
},

/**
* The list of `<vaadin-accordion-panel>` child elements.
* It is populated from the elements passed to the light DOM,
* and updated dynamically when adding or removing panels.
* @type {!Array<!AccordionPanel>}
*/
items: {
type: Array,
readOnly: true,
notify: true,
},
};
}

static get observers() {
return ['_updateItems(items, opened)'];
}

constructor() {
super();
this._boundUpdateOpened = this._updateOpened.bind(this);
}

/**
* Override getter from `KeyboardDirectionMixin`
* to check if the heading element has focus.
*
* @return {Element | null}
* @protected
* @override
*/
get focused() {
return (this._getItems() || []).find((item) => isElementFocused(item.focusElement));
}

/**
* @protected
* @override
*/
focus() {
if (this._observer) {
this._observer.flush();
}
super.focus();
}

/** @protected */
ready() {
super.ready();

const slot = this.shadowRoot.querySelector('slot');
this._observer = new SlotObserver(slot, (info) => {
this._setItems(this._filterItems(Array.from(this.children)));

this._filterItems(info.addedNodes).forEach((el) => {
el.addEventListener('opened-changed', this._boundUpdateOpened);
});
});
}

/**
* Override method inherited from `KeyboardDirectionMixin`
* to use the stored list of accordion panels as items.
*
* @return {Element[]}
* @protected
* @override
*/
_getItems() {
return this.items;
}

/**
* @param {!Array<!Element>} array
* @return {!Array<!AccordionPanel>}
* @protected
*/
_filterItems(array) {
return array.filter((el) => el instanceof customElements.get('vaadin-accordion-panel'));
}

/** @private */
_updateItems(items, opened) {
if (items) {
const itemToOpen = items[opened];
items.forEach((item) => {
item.opened = item === itemToOpen;
});
}
}

/**
* Override an event listener from `KeyboardMixin`
* to only handle details toggle buttons events.
*
* @param {!KeyboardEvent} event
* @protected
* @override
*/
_onKeyDown(event) {
// Only check keyboard events on details toggle buttons
if (!this.items.some((item) => item.focusElement === event.target)) {
return;
}

super._onKeyDown(event);
}

/** @private */
_updateOpened(e) {
const target = this._filterItems(e.composedPath())[0];
const idx = this.items.indexOf(target);
if (e.detail.value) {
if (target.disabled || idx === -1) {
return;
}

this.opened = idx;
} else if (!this.items.some((item) => item.opened)) {
this.opened = null;
}
}
};
28 changes: 28 additions & 0 deletions packages/accordion/src/vaadin-accordion-panel-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @license
* Copyright (c) 2019 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import type { Constructor } from '@open-wc/dedupe-mixin';
import type { DelegateFocusMixinClass } from '@vaadin/a11y-base/src/delegate-focus-mixin.js';
import type { DelegateStateMixinClass } from '@vaadin/component-base/src/delegate-state-mixin.js';
import type { CollapsibleMixinClass } from '@vaadin/details/src/collapsible-mixin.js';

/**
* A mixin providing common accordion panel functionality.
*/
export declare function AccordionPanelMixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<AccordionPanelMixinClass> &
Constructor<CollapsibleMixinClass> &
Constructor<DelegateFocusMixinClass> &
Constructor<DelegateStateMixinClass> &
T;

export declare class AccordionPanelMixinClass {
/**
* A text that is displayed in the heading, if no
* element is assigned to the `summary` slot.
*/
summary: string | null | undefined;
}
105 changes: 105 additions & 0 deletions packages/accordion/src/vaadin-accordion-panel-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @license
* Copyright (c) 2019 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { DelegateFocusMixin } from '@vaadin/a11y-base/src/delegate-focus-mixin.js';
import { DelegateStateMixin } from '@vaadin/component-base/src/delegate-state-mixin.js';
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
import { CollapsibleMixin } from '@vaadin/details/src/collapsible-mixin.js';
import { SummaryController } from '@vaadin/details/src/summary-controller.js';

/**
* A mixin providing common accordion panel functionality.
*
* @polymerMixin
* @mixes CollapsibleMixin
* @mixes DelegateFocusMixin
* @mixes DelegateStateMixin
*/
export const AccordionPanelMixin = (superClass) =>
class AccordionPanelMixinClass extends CollapsibleMixin(DelegateFocusMixin(DelegateStateMixin(superClass))) {
static get properties() {
return {
/**
* A text that is displayed in the heading, if no
* element is assigned to the `summary` slot.
*/
summary: {
type: String,
observer: '_summaryChanged',
},
};
}

static get observers() {
return ['__updateAriaAttributes(focusElement, _contentElements)'];
}

static get delegateAttrs() {
return ['theme'];
}

static get delegateProps() {
return ['disabled', 'opened'];
}

constructor() {
super();

this._summaryController = new SummaryController(this, 'vaadin-accordion-heading');
this._summaryController.addEventListener('slot-content-changed', (event) => {
const { node } = event.target;

this._setFocusElement(node);
this.stateTarget = node;

this._tooltipController.setTarget(node);
});

this._tooltipController = new TooltipController(this);
this._tooltipController.setPosition('bottom-start');
}

/** @protected */
ready() {
super.ready();

this.addController(this._summaryController);
this.addController(this._tooltipController);
}

/**
* Override method inherited from `DisabledMixin`
* to not set `aria-disabled` on the host element.
*
* @protected
* @override
*/
_setAriaDisabled() {
// The `aria-disabled` is set on the details summary.
}

/** @private */
_summaryChanged(summary) {
this._summaryController.setSummary(summary);
}

/** @private */
__updateAriaAttributes(focusElement, contentElements) {
if (focusElement && contentElements) {
const node = contentElements[0];

if (node) {
node.setAttribute('role', 'region');
node.setAttribute('aria-labelledby', focusElement.id);
}

if (node && node.id) {
focusElement.setAttribute('aria-controls', node.id);
} else {
focusElement.removeAttribute('aria-controls');
}
}
}
};
14 changes: 2 additions & 12 deletions packages/accordion/src/vaadin-accordion-panel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
* Copyright (c) 2019 - 2023 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { DelegateFocusMixin } from '@vaadin/a11y-base/src/delegate-focus-mixin.js';
import { DelegateStateMixin } from '@vaadin/component-base/src/delegate-state-mixin.js';
import { CollapsibleMixin } from '@vaadin/details/src/collapsible-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { AccordionPanelMixin } from './vaadin-accordion-panel-mixin.js';

/**
* Fired when the `opened` property changes.
Expand Down Expand Up @@ -43,15 +41,7 @@ export type AccordionPanelEventMap = AccordionPanelCustomEventMap & HTMLElementE
*
* @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
*/
declare class AccordionPanel extends CollapsibleMixin(
DelegateFocusMixin(DelegateStateMixin(ThemableMixin(HTMLElement))),
) {
/**
* A text that is displayed in the heading, if no
* element is assigned to the `summary` slot.
*/
summary: string | null | undefined;

declare class AccordionPanel extends AccordionPanelMixin(ThemableMixin(HTMLElement)) {
addEventListener<K extends keyof AccordionPanelEventMap>(
type: K,
listener: (this: AccordionPanel, ev: AccordionPanelEventMap[K]) => void,
Expand Down
Loading

0 comments on commit 7d4cabc

Please sign in to comment.