1/** 2 * @class TicketGroupSection 3 * @extends HTMLElement 4 * 5 * @classdesc Collapsible section wrapping a group of `<ticket-card-layout>` cards (see 6 * js/Layouts/TicketCard.js) under a titled, caret-toggled header. Purely a structural/display 7 * wrapper -- which tickets belong to which group, and in what order groups/sections appear on 8 * the page, is decided by the consuming page, not here. In particular this component keeps no 9 * internal registry keyed by `group-id`, so more than one section may share the same 10 * `group-id`/`group-title` on the same page without conflict -- a single logical group might 11 * need to render as more than one non-contiguous section if tickets end up sorted by something 12 * other than group (e.g. by name). 13 * 14 * @example 15 * <ticket-group-section group-id="4" group-title="VIP Access" default-open> 16 * <ticket-card-layout ...></ticket-card-layout> 17 * <ticket-card-layout ...></ticket-card-layout> 18 * </ticket-group-section> 19 * 20 * @attribute {string} group-id - Opaque id echoed back in the group-toggled event detail. 21 * @attribute {string} group-title - Header text. 22 * @attribute {boolean} default-open - Section starts expanded when present. Default: collapsed. 23 * 24 * @slot (default) - The group's `<ticket-card-layout>` children. 25 * 26 * @method setOpen(open) - Explicitly opens/collapses the section. 27 * @method isOpen() - Returns the current expanded state. 28 * 29 * @event group-toggled - Fired after the section's open state changes (header click, Enter/Space, 30 * or a setOpen() call). detail: {groupId, open} 31 */ 32class TicketGroupSection extends HTMLElement { 33 static get observedAttributes() { 34 return ['group-title']; 35 } 36 37 constructor() { 38 super(); 39 this.attachShadow({ mode: 'open' }); 40 41 this._open = false; 42 this._contentId = `ticket-group-content-${Math.random().toString(36).slice(2, 9)}`; 43 44 const style = document.createElement('style'); 45 style.textContent = ` 46 @import url('https://unicons.iconscout.com/release/v4.0.8/css/line.css'); 47 48 :host { 49 display: block; 50 font-family: 'Plus Jakarta Sans', 'Roboto', sans-serif; 51 } 52 53 .group-header { 54 all: unset; 55 box-sizing: border-box; 56 display: flex; 57 align-items: center; 58 justify-content: space-between; 59 width: 100%; 60 padding: 12px 4px; 61 cursor: pointer; 62 font-size: 18px; 63 font-weight: 600; 64 color: var(--text-primary, #222222); 65 } 66 67 .group-header:focus-visible { 68 outline: 2px solid var(--border-focused, #026E80); 69 outline-offset: 2px; 70 border-radius: 4px; 71 } 72 73 .group-caret { 74 display: flex; 75 align-items: center; 76 justify-content: center; 77 flex-shrink: 0; 78 transition: transform 0.2s ease; 79 } 80 81 .group-caret i { 82 font-size: 20px; 83 line-height: 1; 84 } 85 86 :host([open]) .group-caret { 87 transform: rotate(180deg); 88 } 89 90 /* 91 * Fixed large max-height (rather than measuring scrollHeight) is the same 92 * collapse-transition trick already used by js/Components/AccordionCheckbox.js -- 93 * kept consistent with that existing pattern rather than introducing a second 94 * collapse technique into the codebase. 95 */ 96 /* 97 * display:flex + gap (not ::slotted margin-top) spaces the cards -- a <slot>'s 98 * assigned nodes flatten into the flex layout as if they were direct children, so 99 * gap applies between them same as js/events/ticket-purchase-page.js's own 100 * #ticket-cards-root. This sidesteps the page-wide * {margin:0} reset in 101 * css/base-2017-05-15.css entirely instead of relying on ::slotted's specificity 102 * to beat it. 103 */ 104 .group-content { 105 display: flex; 106 flex-direction: column; 107 gap: 16px; 108 max-height: 0; 109 overflow: hidden; 110 transition: max-height 0.3s ease-out; 111 } 112 113 .group-content.show { 114 max-height: 4000px; 115 transition: max-height 0.5s ease-in; 116 } 117 118 /* 119 * Mobile: the group becomes a full-bleed list block (its container, 120 * #ticket-cards-root, is made edge-to-edge at the page level for exactly this -- 121 * see css/events/ticket.css). The header's hairline borders bracket the title and 122 * reach the true screen edges because they're drawn on .group-header itself, which 123 * is the full, unpadded width of :host -- .group-header's own padding sits INSIDE 124 * that border, so it never insets where the border line is drawn. gap drops to 0 here 125 * since the row spacing/divider between cards comes from each card's own 126 * ".ticket-row" wrapper below instead (a plain div the card itself is never aware of -- 127 * see TicketListController.js's appendTicket()); cards keep their own real border only 128 * for selected/cart-added/error (js/Layouts/TicketCard.js's own mobile block). 129 */ 130 @media (max-width: 640px) { 131 .group-header { 132 padding: 12px 10px; 133 border-top: 0.5px solid #C4CAC9; 134 border-bottom: 0.5px solid #C4CAC9; 135 } 136 137 .group-content { 138 gap: 0; 139 } 140 141 /* 142 * !important beats css/base-2017-05-15.css's global *{border:0;margin:0;padding:0;} 143 * / div{...} resets outright, rather than leaning on ::slotted(.ticket-row)'s own 144 * (0,1,1) specificity to out-rank them across the shadow boundary. 145 */ 146 ::slotted(.ticket-row) { 147 padding: 12px 8px !important; 148 border-bottom: 0.5px solid #C4CAC9 !important; 149 } 150 151 ::slotted(.ticket-row:last-child) { 152 border-bottom: none !important; 153 } 154 } 155 `; 156 157 const header = document.createElement('button'); 158 header.type = 'button'; 159 header.classList.add('group-header'); 160 header.setAttribute('aria-expanded', 'false'); 161 header.setAttribute('aria-controls', this._contentId); 162 163 this._titleEl = document.createElement('span'); 164 this._titleEl.classList.add('group-title'); 165 166 const caret = document.createElement('span'); 167 caret.classList.add('group-caret'); 168 caret.setAttribute('aria-hidden', 'true'); 169 caret.innerHTML = `<i class="uil uil-angle-down"></i>`; 170 171 header.appendChild(this._titleEl); 172 header.appendChild(caret); 173 174 const content = document.createElement('div'); 175 content.classList.add('group-content'); 176 content.id = this._contentId; 177 content.appendChild(document.createElement('slot')); 178 179 this.shadowRoot.appendChild(style); 180 this.shadowRoot.appendChild(header); 181 this.shadowRoot.appendChild(content); 182 183 this._header = header; 184 this._content = content; 185 186 header.addEventListener('click', () => this.setOpen(!this._open)); 187 } 188 189 connectedCallback() { 190 // createElement() runs the constructor before the caller has a chance to call 191 // setAttribute('default-open', ...), so the attribute can only be read reliably here, 192 // once the element has actually been inserted with all its attributes already set. 193 this._open = this.hasAttribute('default-open'); 194 this._titleEl.textContent = this.getAttribute('group-title') || ''; 195 this._applyOpenState({ silent: true }); 196 } 197 198 attributeChangedCallback(name, oldValue, newValue) { 199 if (name === 'group-title' && this._titleEl) { 200 this._titleEl.textContent = newValue || ''; 201 } 202 } 203 204 /** 205 * @param {boolean} open 206 */ 207 setOpen(open) { 208 const next = !!open; 209 if (next === this._open) return; 210 this._open = next; 211 this._applyOpenState(); 212 } 213 214 /** 215 * @returns {boolean} 216 */ 217 isOpen() { 218 return this._open; 219 } 220 221 _applyOpenState({ silent = false } = {}) { 222 this.toggleAttribute('open', this._open); 223 this._header.setAttribute('aria-expanded', String(this._open)); 224 this._content.classList.toggle('show', this._open); 225 226 if (!silent) { 227 this.dispatchEvent(new CustomEvent('group-toggled', { 228 detail: { groupId: this.getAttribute('group-id'), open: this._open }, 229 bubbles: true, 230 composed: true 231 })); 232 } 233 } 234} 235 236if (!customElements.get('ticket-group-section')) { 237 customElements.define('ticket-group-section', TicketGroupSection); 238}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.