UNPKG

15 kBJavaScriptView Raw
1/*!
2 * (C) Ionic http://ionicframework.com - MIT License
3 */
4import { proxyCustomElement, HTMLElement, h, Host } from '@stencil/core/internal/client';
5import { c as chevronDown } from './index6.js';
6import { c as config, b as getIonMode } from './ionic-global.js';
7import { r as raf, t as transitionEndAsync, a as addEventListener, b as removeEventListener, g as getElementRoot } from './helpers.js';
8import { d as defineCustomElement$2 } from './icon.js';
9
10const accordionIosCss = ":host{display:block;position:relative;width:100%;background-color:var(--ion-background-color, #ffffff);overflow:hidden;z-index:0}:host(.accordion-expanding) ::slotted(ion-item[slot=header]),:host(.accordion-expanded) ::slotted(ion-item[slot=header]){--border-width:0px}:host(.accordion-animated){-webkit-transition:all 300ms cubic-bezier(0.25, 0.8, 0.5, 1);transition:all 300ms cubic-bezier(0.25, 0.8, 0.5, 1)}:host(.accordion-animated) #content{-webkit-transition:max-height 300ms cubic-bezier(0.25, 0.8, 0.5, 1);transition:max-height 300ms cubic-bezier(0.25, 0.8, 0.5, 1)}#content{overflow:hidden;will-change:max-height}:host(.accordion-collapsing) #content{max-height:0 !important}:host(.accordion-collapsed) #content{display:none}:host(.accordion-expanding) #content{max-height:0}:host(.accordion-disabled) #header,:host(.accordion-readonly) #header,:host(.accordion-disabled) #content,:host(.accordion-readonly) #content{pointer-events:none}:host(.accordion-disabled) #header,:host(.accordion-disabled) #content{opacity:0.4}@media (prefers-reduced-motion: reduce){:host,#content{-webkit-transition:none !important;transition:none !important}}:host(.accordion-next) ::slotted(ion-item[slot=header]){--border-width:0.55px 0px 0.55px 0px}";
11
12const accordionMdCss = ":host{display:block;position:relative;width:100%;background-color:var(--ion-background-color, #ffffff);overflow:hidden;z-index:0}:host(.accordion-expanding) ::slotted(ion-item[slot=header]),:host(.accordion-expanded) ::slotted(ion-item[slot=header]){--border-width:0px}:host(.accordion-animated){-webkit-transition:all 300ms cubic-bezier(0.25, 0.8, 0.5, 1);transition:all 300ms cubic-bezier(0.25, 0.8, 0.5, 1)}:host(.accordion-animated) #content{-webkit-transition:max-height 300ms cubic-bezier(0.25, 0.8, 0.5, 1);transition:max-height 300ms cubic-bezier(0.25, 0.8, 0.5, 1)}#content{overflow:hidden;will-change:max-height}:host(.accordion-collapsing) #content{max-height:0 !important}:host(.accordion-collapsed) #content{display:none}:host(.accordion-expanding) #content{max-height:0}:host(.accordion-disabled) #header,:host(.accordion-readonly) #header,:host(.accordion-disabled) #content,:host(.accordion-readonly) #content{pointer-events:none}:host(.accordion-disabled) #header,:host(.accordion-disabled) #content{opacity:0.4}@media (prefers-reduced-motion: reduce){:host,#content{-webkit-transition:none !important;transition:none !important}}";
13
14const Accordion = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
15 constructor() {
16 super();
17 this.__registerHost();
18 this.__attachShadow();
19 this.updateListener = () => this.updateState(false);
20 this.state = 1 /* Collapsed */;
21 this.isNext = false;
22 this.isPrevious = false;
23 /**
24 * The value of the accordion. Defaults to an autogenerated
25 * value.
26 */
27 this.value = `ion-accordion-${accordionIds++}`;
28 /**
29 * If `true`, the accordion cannot be interacted with.
30 */
31 this.disabled = false;
32 /**
33 * If `true`, the accordion cannot be interacted with,
34 * but does not alter the opacity.
35 */
36 this.readonly = false;
37 /**
38 * The toggle icon to use. This icon will be
39 * rotated when the accordion is expanded
40 * or collapsed.
41 */
42 this.toggleIcon = chevronDown;
43 /**
44 * The slot inside of `ion-item` to
45 * place the toggle icon. Defaults to `'end'`.
46 */
47 this.toggleIconSlot = 'end';
48 this.setItemDefaults = () => {
49 const ionItem = this.getSlottedHeaderIonItem();
50 if (!ionItem) {
51 return;
52 }
53 /**
54 * For a11y purposes, we make
55 * the ion-item a button so users
56 * can tab to it and use keyboard
57 * navigation to get around.
58 */
59 ionItem.button = true;
60 ionItem.detail = false;
61 /**
62 * By default, the lines in an
63 * item should be full here, but
64 * only do that if a user has
65 * not explicitly overridden them
66 */
67 if (ionItem.lines === undefined) {
68 ionItem.lines = 'full';
69 }
70 };
71 this.getSlottedHeaderIonItem = () => {
72 const { headerEl } = this;
73 if (!headerEl) {
74 return;
75 }
76 /**
77 * Get the first ion-item
78 * slotted in the header slot
79 */
80 const slot = headerEl.querySelector('slot');
81 if (!slot) {
82 return;
83 }
84 // This is not defined in unit tests
85 const ionItem = slot.assignedElements && slot.assignedElements().find(el => el.tagName === 'ION-ITEM');
86 return ionItem;
87 };
88 this.setAria = (expanded = false) => {
89 const ionItem = this.getSlottedHeaderIonItem();
90 if (!ionItem) {
91 return;
92 }
93 /**
94 * Get the native <button> element inside of
95 * ion-item because that is what will be focused
96 */
97 const root = getElementRoot(ionItem);
98 const button = root.querySelector('button');
99 if (!button) {
100 return;
101 }
102 button.setAttribute('aria-expanded', `${expanded}`);
103 };
104 this.slotToggleIcon = () => {
105 const ionItem = this.getSlottedHeaderIonItem();
106 if (!ionItem) {
107 return;
108 }
109 const { toggleIconSlot, toggleIcon } = this;
110 /**
111 * Check if there already is a toggle icon.
112 * If so, do not add another one.
113 */
114 const existingToggleIcon = ionItem.querySelector('.ion-accordion-toggle-icon');
115 if (existingToggleIcon) {
116 return;
117 }
118 const iconEl = document.createElement('ion-icon');
119 iconEl.slot = toggleIconSlot;
120 iconEl.lazy = false;
121 iconEl.classList.add('ion-accordion-toggle-icon');
122 iconEl.icon = toggleIcon;
123 iconEl.setAttribute('aria-hidden', 'true');
124 ionItem.appendChild(iconEl);
125 };
126 this.expandAccordion = (initialUpdate = false) => {
127 if (initialUpdate) {
128 this.state = 4 /* Expanded */;
129 return;
130 }
131 if (this.state === 4 /* Expanded */) {
132 return;
133 }
134 const { contentEl, contentElWrapper } = this;
135 if (contentEl === undefined || contentElWrapper === undefined) {
136 return;
137 }
138 if (this.currentRaf !== undefined) {
139 cancelAnimationFrame(this.currentRaf);
140 }
141 if (this.shouldAnimate()) {
142 raf(() => {
143 this.state = 8 /* Expanding */;
144 this.currentRaf = raf(async () => {
145 const contentHeight = contentElWrapper.offsetHeight;
146 const waitForTransition = transitionEndAsync(contentEl, 2000);
147 contentEl.style.setProperty('max-height', `${contentHeight}px`);
148 await waitForTransition;
149 this.state = 4 /* Expanded */;
150 contentEl.style.removeProperty('max-height');
151 });
152 });
153 }
154 else {
155 this.state = 4 /* Expanded */;
156 }
157 };
158 this.collapseAccordion = (initialUpdate = false) => {
159 if (initialUpdate) {
160 this.state = 1 /* Collapsed */;
161 return;
162 }
163 if (this.state === 1 /* Collapsed */) {
164 return;
165 }
166 const { contentEl } = this;
167 if (contentEl === undefined) {
168 return;
169 }
170 if (this.currentRaf !== undefined) {
171 cancelAnimationFrame(this.currentRaf);
172 }
173 if (this.shouldAnimate()) {
174 this.currentRaf = raf(async () => {
175 const contentHeight = contentEl.offsetHeight;
176 contentEl.style.setProperty('max-height', `${contentHeight}px`);
177 raf(async () => {
178 const waitForTransition = transitionEndAsync(contentEl, 2000);
179 this.state = 2 /* Collapsing */;
180 await waitForTransition;
181 this.state = 1 /* Collapsed */;
182 contentEl.style.removeProperty('max-height');
183 });
184 });
185 }
186 else {
187 this.state = 1 /* Collapsed */;
188 }
189 };
190 /**
191 * Helper function to determine if
192 * something should animate.
193 * If prefers-reduced-motion is set
194 * then we should not animate, regardless
195 * of what is set in the config.
196 */
197 this.shouldAnimate = () => {
198 if (typeof window === 'undefined') {
199 return false;
200 }
201 const prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
202 if (prefersReducedMotion) {
203 return false;
204 }
205 const animated = config.get('animated', true);
206 if (!animated) {
207 return false;
208 }
209 if (this.accordionGroupEl && !this.accordionGroupEl.animated) {
210 return false;
211 }
212 return true;
213 };
214 this.updateState = async (initialUpdate = false) => {
215 const accordionGroup = this.accordionGroupEl;
216 const accordionValue = this.value;
217 if (!accordionGroup) {
218 return;
219 }
220 const value = accordionGroup.value;
221 const shouldExpand = (Array.isArray(value)) ? value.includes(accordionValue) : value === accordionValue;
222 if (shouldExpand) {
223 this.expandAccordion(initialUpdate);
224 this.isNext = this.isPrevious = false;
225 }
226 else {
227 this.collapseAccordion(initialUpdate);
228 /**
229 * When using popout or inset,
230 * the collapsed accordion items
231 * may need additional border radius
232 * applied. Check to see if the
233 * next or previous accordion is selected.
234 */
235 const nextAccordion = this.getNextSibling();
236 const nextAccordionValue = nextAccordion && nextAccordion.value;
237 if (nextAccordionValue !== undefined) {
238 this.isPrevious = (Array.isArray(value)) ? value.includes(nextAccordionValue) : value === nextAccordionValue;
239 }
240 const previousAccordion = this.getPreviousSibling();
241 const previousAccordionValue = previousAccordion && previousAccordion.value;
242 if (previousAccordionValue !== undefined) {
243 this.isNext = (Array.isArray(value)) ? value.includes(previousAccordionValue) : value === previousAccordionValue;
244 }
245 }
246 };
247 this.getNextSibling = () => {
248 if (!this.el) {
249 return;
250 }
251 const nextSibling = this.el.nextElementSibling;
252 if ((nextSibling === null || nextSibling === void 0 ? void 0 : nextSibling.tagName) !== 'ION-ACCORDION') {
253 return;
254 }
255 return nextSibling;
256 };
257 this.getPreviousSibling = () => {
258 if (!this.el) {
259 return;
260 }
261 const previousSibling = this.el.previousElementSibling;
262 if ((previousSibling === null || previousSibling === void 0 ? void 0 : previousSibling.tagName) !== 'ION-ACCORDION') {
263 return;
264 }
265 return previousSibling;
266 };
267 }
268 connectedCallback() {
269 const accordionGroupEl = this.accordionGroupEl = this.el && this.el.closest('ion-accordion-group');
270 if (accordionGroupEl) {
271 this.updateState(true);
272 addEventListener(accordionGroupEl, 'ionChange', this.updateListener);
273 }
274 }
275 disconnectedCallback() {
276 const accordionGroupEl = this.accordionGroupEl;
277 if (accordionGroupEl) {
278 removeEventListener(accordionGroupEl, 'ionChange', this.updateListener);
279 }
280 }
281 componentDidLoad() {
282 this.setItemDefaults();
283 this.slotToggleIcon();
284 /**
285 * We need to wait a tick because we
286 * just set ionItem.button = true and
287 * the button has not have been rendered yet.
288 */
289 raf(() => {
290 /**
291 * Set aria label on button inside of ion-item
292 * once the inner content has been rendered.
293 */
294 const expanded = this.state === 4 /* Expanded */ || this.state === 8 /* Expanding */;
295 this.setAria(expanded);
296 });
297 }
298 toggleExpanded() {
299 const { accordionGroupEl, value, state } = this;
300 if (accordionGroupEl) {
301 /**
302 * Because the accordion group may or may
303 * not allow multiple accordions open, we
304 * need to request the toggling of this
305 * accordion and the accordion group will
306 * make the decision on whether or not
307 * to allow it.
308 */
309 const expand = state === 1 /* Collapsed */ || state === 2 /* Collapsing */;
310 accordionGroupEl.requestAccordionToggle(value, expand);
311 }
312 }
313 render() {
314 const { disabled, readonly } = this;
315 const mode = getIonMode(this);
316 const expanded = this.state === 4 /* Expanded */ || this.state === 8 /* Expanding */;
317 const headerPart = expanded ? 'header expanded' : 'header';
318 const contentPart = expanded ? 'content expanded' : 'content';
319 this.setAria(expanded);
320 return (h(Host, { class: {
321 [mode]: true,
322 'accordion-expanding': this.state === 8 /* Expanding */,
323 'accordion-expanded': this.state === 4 /* Expanded */,
324 'accordion-collapsing': this.state === 2 /* Collapsing */,
325 'accordion-collapsed': this.state === 1 /* Collapsed */,
326 'accordion-next': this.isNext,
327 'accordion-previous': this.isPrevious,
328 'accordion-disabled': disabled,
329 'accordion-readonly': readonly,
330 'accordion-animated': config.getBoolean('animated', true)
331 } }, h("div", { onClick: () => this.toggleExpanded(), id: "header", part: headerPart, "aria-controls": "content", ref: headerEl => this.headerEl = headerEl }, h("slot", { name: "header" })), h("div", { id: "content", part: contentPart, role: "region", "aria-labelledby": "header", ref: contentEl => this.contentEl = contentEl }, h("div", { id: "content-wrapper", ref: contentElWrapper => this.contentElWrapper = contentElWrapper }, h("slot", { name: "content" })))));
332 }
333 static get delegatesFocus() { return true; }
334 get el() { return this; }
335 static get style() { return {
336 ios: accordionIosCss,
337 md: accordionMdCss
338 }; }
339}, [49, "ion-accordion", {
340 "value": [1],
341 "disabled": [4],
342 "readonly": [4],
343 "toggleIcon": [1, "toggle-icon"],
344 "toggleIconSlot": [1, "toggle-icon-slot"],
345 "state": [32],
346 "isNext": [32],
347 "isPrevious": [32]
348 }]);
349let accordionIds = 0;
350function defineCustomElement$1() {
351 if (typeof customElements === "undefined") {
352 return;
353 }
354 const components = ["ion-accordion", "ion-icon"];
355 components.forEach(tagName => { switch (tagName) {
356 case "ion-accordion":
357 if (!customElements.get(tagName)) {
358 customElements.define(tagName, Accordion);
359 }
360 break;
361 case "ion-icon":
362 if (!customElements.get(tagName)) {
363 defineCustomElement$2();
364 }
365 break;
366 } });
367}
368
369const IonAccordion = Accordion;
370const defineCustomElement = defineCustomElement$1;
371
372export { IonAccordion, defineCustomElement };