Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: extract accordion and accordion-panel mixins #6975

Merged
merged 1 commit into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
};
34 changes: 34 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,34 @@
/**
* @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 { DisabledMixinClass } from '@vaadin/a11y-base/src/disabled-mixin.js';
import type { FocusMixinClass } from '@vaadin/a11y-base/src/focus-mixin.js';
import type { TabindexMixinClass } from '@vaadin/a11y-base/src/tabindex-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> &
Constructor<DisabledMixinClass> &
Constructor<FocusMixinClass> &
Constructor<TabindexMixinClass> &
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