1/** 2 * @class FaqController 3 * @extends Controller 4 * @description 5 * Accordion-style expand/collapse for FAQ sections or collapsible content. 6 * Uses max-height animation for smooth open/close transitions. 7 * 8 * Features: 9 * - Smooth height animation using CSS transitions 10 * - Sets "open" attribute on element for CSS styling 11 * - ARIA accessibility support 12 * - Individual or grouped FAQ items 13 * 14 * @example <caption>Basic FAQ item</caption> 15 * <div data-controller="faq"> 16 * <button data-action="click->faq#toggle" class="flex justify-between w-full"> 17 * <span>What is your return policy?</span> 18 * <i class="fa-chevron-down transition-transform [:is([open])_&]:rotate-180"></i> 19 * </button> 20 * 21 * <div data-faq-target="content" class="overflow-hidden transition-all duration-300"> 22 * <p class="py-4">We offer a 30-day money-back guarantee...</p> 23 * </div> 24 * </div> 25 * 26 * @example <caption>Multiple FAQ items</caption> 27 * - faqs.each do |faq| 28 * div[data-controller="faq"] 29 * button[data-action="click->faq#toggle"]= faq.question 30 * div[data-faq-target="content"]= faq.answer 31 */ 32import { Controller } from '@hotwired/stimulus' 33 34export default class FaqController extends Controller { 35 /** 36 * Stimulus targets for this controller. 37 * @static 38 * @property {string[]} targets 39 * @property {HTMLElement} contentTarget - The collapsible content container 40 * @property {HTMLElement} triggerTarget - Optional trigger element for ARIA (optional) 41 */ 42 static targets = ['content', 'trigger'] 43 44 /** 45 * Whether the FAQ item is currently expanded. 46 * @type {boolean} 47 */ 48 isOpen = false 49 50 /** 51 * Initializes the controller with content hidden (max-height: 0). 52 * Called automatically when the controller connects to the DOM. 53 * 54 * @method connect 55 * @returns {void} 56 */ 57 connect() { 58 if (!this.hasContentTarget) return 59 60 this.isOpen = false 61 this.contentTarget.classList.remove('hidden') 62 this.contentTarget.style.maxHeight = '0px' 63 } 64 65 /** 66 * Toggles between open and closed states. 67 * 68 * @method toggle 69 * @returns {void} 70 * 71 * @example 72 * <button data-action="click->faq#toggle">Toggle FAQ</button> 73 */ 74 toggle() { 75 if (!this.hasContentTarget) return 76 77 if (this.isOpen) { 78 this.hide() 79 } else { 80 this.open() 81 } 82 } 83 84 /** 85 * Opens the FAQ content with animation. 86 * Sets the "open" attribute on the element for CSS styling. 87 * 88 * @method open 89 * @returns {void} 90 */ 91 open() { 92 if (!this.hasContentTarget || this.isOpen) return 93 94 this.isOpen = true 95 this.contentTarget.style.maxHeight = this.contentTarget.scrollHeight + 'px' 96 this.element.setAttribute('open', '') 97 98 if (this.hasTriggerTarget) { 99 this.triggerTarget.setAttribute('aria-expanded', 'true') 100 } 101 } 102 103 /** 104 * Closes the FAQ content with animation. 105 * Removes the "open" attribute from the element. 106 * 107 * @method hide 108 * @returns {void} 109 */ 110 hide() { 111 if (!this.hasContentTarget || !this.isOpen) return 112 113 this.isOpen = false 114 this.contentTarget.style.maxHeight = '0px' 115 this.element.removeAttribute('open') 116 117 if (this.hasTriggerTarget) { 118 this.triggerTarget.setAttribute('aria-expanded', 'false') 119 } 120 } 121}
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.