basecoat-cli 0.2.0-beta.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ (() => {
2
+ const initPopover = (popoverComponent) => {
3
+ const trigger = popoverComponent.querySelector(':scope > button');
4
+ const content = popoverComponent.querySelector(':scope > [data-popover]');
5
+
6
+ if (!trigger || !content) {
7
+ console.error('Popover component is missing a trigger button or a content element.', popoverComponent);
8
+ return;
9
+ }
10
+
11
+ const closePopover = () => {
12
+ if (trigger.getAttribute('aria-expanded') === 'false') return;
13
+ trigger.setAttribute('aria-expanded', 'false');
14
+ content.setAttribute('aria-hidden', 'true');
15
+ trigger.focus();
16
+ };
17
+
18
+ const openPopover = () => {
19
+ const elementToFocus = content.querySelector('[autofocus]');
20
+ if (elementToFocus) {
21
+ content.addEventListener('transitionend', () => {
22
+ elementToFocus.focus();
23
+ }, { once: true });
24
+ }
25
+
26
+ trigger.setAttribute('aria-expanded', 'true');
27
+ content.setAttribute('aria-hidden', 'false');
28
+ };
29
+
30
+ trigger.addEventListener('click', () => {
31
+ const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
32
+ if (isExpanded) {
33
+ closePopover();
34
+ } else {
35
+ openPopover();
36
+ }
37
+ });
38
+
39
+ popoverComponent.addEventListener('keydown', (e) => {
40
+ if (e.key === 'Escape') {
41
+ closePopover();
42
+ }
43
+ });
44
+
45
+ document.addEventListener('click', (e) => {
46
+ if (!popoverComponent.contains(e.target)) {
47
+ closePopover();
48
+ }
49
+ });
50
+
51
+ popoverComponent.dataset.popoverInitialized = true;
52
+ };
53
+
54
+ document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
55
+
56
+ const observer = new MutationObserver((mutations) => {
57
+ mutations.forEach((mutation) => {
58
+ mutation.addedNodes.forEach((node) => {
59
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
60
+ if (node.matches('.popover:not([data-popover-initialized])')) {
61
+ initPopover(node);
62
+ }
63
+ node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
64
+ });
65
+ });
66
+ });
67
+
68
+ observer.observe(document.body, { childList: true, subtree: true });
69
+ })();
@@ -0,0 +1 @@
1
+ (()=>{const e=e=>{const t=e.querySelector(":scope > button"),o=e.querySelector(":scope > [data-popover]");if(!t||!o)return void console.error("Popover component is missing a trigger button or a content element.",e);const r=()=>{"false"!==t.getAttribute("aria-expanded")&&(t.setAttribute("aria-expanded","false"),o.setAttribute("aria-hidden","true"),t.focus())};t.addEventListener("click",(()=>{"true"===t.getAttribute("aria-expanded")?r():(()=>{const e=o.querySelector("[autofocus]");e&&o.addEventListener("transitionend",(()=>{e.focus()}),{once:!0}),t.setAttribute("aria-expanded","true"),o.setAttribute("aria-hidden","false")})()})),e.addEventListener("keydown",(e=>{"Escape"===e.key&&r()})),document.addEventListener("click",(t=>{e.contains(t.target)||r()})),e.dataset.popoverInitialized=!0};document.querySelectorAll(".popover:not([data-popover-initialized])").forEach(e);new MutationObserver((t=>{t.forEach((t=>{t.addedNodes.forEach((t=>{t.nodeType===Node.ELEMENT_NODE&&(t.matches(".popover:not([data-popover-initialized])")&&e(t),t.querySelectorAll(".popover:not([data-popover-initialized])").forEach(e))}))}))})).observe(document.body,{childList:!0,subtree:!0})})();
@@ -1,8 +1,8 @@
1
1
  (() => {
2
2
  const initSelect = (selectComponent) => {
3
- const trigger = selectComponent.querySelector(':scope > [popovertarget]');
3
+ const trigger = selectComponent.querySelector(':scope > button');
4
4
  const selectedValue = trigger.querySelector(':scope > span');
5
- const popover = selectComponent.querySelector(':scope > [popover]');
5
+ const popover = selectComponent.querySelector(':scope > [data-popover]');
6
6
  const listbox = popover.querySelector('[role="listbox"]');
7
7
  const input = selectComponent.querySelector(':scope > input[type="hidden"]');
8
8
  const filter = selectComponent.querySelector('header input[type="text"]');
@@ -21,15 +21,26 @@
21
21
  }
22
22
  };
23
23
 
24
+ const closePopover = () => {
25
+ popover.setAttribute('aria-hidden', 'true');
26
+ trigger.setAttribute('aria-expanded', 'false');
27
+ if (filter) {
28
+ filter.value = '';
29
+ visibleOptions = [...options];
30
+ options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
31
+ }
32
+ trigger.removeAttribute('aria-activedescendant');
33
+ if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
34
+ activeIndex = -1;
35
+ }
36
+
24
37
  const selectOption = (option) => {
25
38
  if (!option) return;
26
39
 
27
- updateValue(option);
28
-
29
- trigger.removeAttribute('aria-activedescendant');
30
- options.forEach(opt => opt.classList.remove('active'));
31
- activeIndex = -1;
32
- popover.hidePopover();
40
+ if (option.dataset.value) {
41
+ updateValue(option);
42
+ }
43
+ closePopover();
33
44
  };
34
45
 
35
46
  if (filter) {
@@ -62,12 +73,14 @@
62
73
  updateValue(initialOption);
63
74
 
64
75
  const handleKeyNavigation = (e) => {
65
- if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End'].includes(e.key)) {
76
+ const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
77
+
78
+ if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(e.key)) {
66
79
  return;
67
80
  }
68
81
 
69
- if (!popover.matches(':popover-open')) {
70
- if (e.currentTarget === trigger && e.key !== 'Enter') {
82
+ if (!isPopoverOpen) {
83
+ if (e.key !== 'Enter' && e.key !== 'Escape') {
71
84
  e.preventDefault();
72
85
  trigger.click();
73
86
  }
@@ -76,9 +89,14 @@
76
89
 
77
90
  e.preventDefault();
78
91
 
92
+ if (e.key === 'Escape') {
93
+ closePopover();
94
+ return;
95
+ }
96
+
79
97
  if (e.key === 'Enter') {
80
98
  if (activeIndex > -1) {
81
- selectOption(options[activeIndex]);
99
+ selectOption(visibleOptions[activeIndex]);
82
100
  }
83
101
  return;
84
102
  }
@@ -130,6 +148,31 @@
130
148
  filter.addEventListener('keydown', handleKeyNavigation);
131
149
  }
132
150
 
151
+ trigger.addEventListener('click', () => {
152
+ const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
153
+
154
+ if (isExpanded) {
155
+ closePopover();
156
+ } else {
157
+ popover.setAttribute('aria-hidden', 'false');
158
+ trigger.setAttribute('aria-expanded', 'true');
159
+ if (filter) filter.focus();
160
+
161
+ const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
162
+ if (selectedOption) {
163
+ if (activeIndex > -1) {
164
+ options[activeIndex]?.classList.remove('active');
165
+ }
166
+ activeIndex = options.indexOf(selectedOption);
167
+ selectedOption.classList.add('active');
168
+ if (selectedOption.id) {
169
+ trigger.setAttribute('aria-activedescendant', selectedOption.id);
170
+ }
171
+ selectedOption.scrollIntoView({ block: 'nearest' });
172
+ }
173
+ }
174
+ });
175
+
133
176
  listbox.addEventListener('click', (e) => {
134
177
  const clickedOption = e.target.closest('[role="option"]');
135
178
  if (clickedOption) {
@@ -137,46 +180,13 @@
137
180
  }
138
181
  });
139
182
 
140
- popover.addEventListener('toggle', (e) => {
141
- trigger.setAttribute('aria-expanded', e.newState === 'open');
142
-
143
- if (e.newState === 'open') {
144
- if (filter) filter.focus();
145
-
146
- const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
147
- let startingOption = null;
148
-
149
- if (selectedOption && visibleOptions.includes(selectedOption)) {
150
- startingOption = selectedOption;
151
- } else if (visibleOptions.length > 0) {
152
- startingOption = visibleOptions[0];
153
- }
154
-
155
- if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
156
-
157
- if (startingOption) {
158
- activeIndex = options.indexOf(startingOption);
159
- startingOption.classList.add('active');
160
- if (startingOption.id) {
161
- trigger.setAttribute('aria-activedescendant', startingOption.id);
162
- }
163
- startingOption.scrollIntoView({ block: 'nearest' });
164
- } else {
165
- activeIndex = -1;
166
- }
167
- } else if (e.newState === 'closed') {
168
- if (filter) {
169
- filter.value = '';
170
- visibleOptions = [...options];
171
- options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
172
- }
173
-
174
- trigger.removeAttribute('aria-activedescendant');
175
- if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
176
- activeIndex = -1;
183
+ document.addEventListener('click', (e) => {
184
+ if (!selectComponent.contains(e.target)) {
185
+ closePopover();
177
186
  }
178
187
  });
179
188
 
189
+ popover.setAttribute('aria-hidden', 'true');
180
190
  selectComponent.dataset.selectInitialized = true;
181
191
  };
182
192
 
@@ -1 +1 @@
1
- (()=>{const e=e=>{const t=e.querySelector(":scope > [popovertarget]"),r=t.querySelector(":scope > span"),a=e.querySelector(":scope > [popover]"),i=a.querySelector('[role="listbox"]'),o=e.querySelector(':scope > input[type="hidden"]'),n=e.querySelector('header input[type="text"]');if(!(t&&a&&i&&o))return;const s=Array.from(i.querySelectorAll('[role="option"]'));let c=[...s],l=-1;const d=e=>{e&&(r.innerHTML=e.dataset.label||e.innerHTML,o.value=e.dataset.value,i.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute("aria-selected"),e.setAttribute("aria-selected","true"))},u=e=>{e&&(d(e),t.removeAttribute("aria-activedescendant"),s.forEach((e=>e.classList.remove("active"))),l=-1,a.hidePopover())};if(n){const e=()=>{const e=n.value.trim().toLowerCase();l>-1&&(s[l].classList.remove("active"),t.removeAttribute("aria-activedescendant"),l=-1),c=[],s.forEach((t=>{const r=(t.dataset.label||t.textContent).trim().toLowerCase().includes(e);t.setAttribute("aria-hidden",String(!r)),r&&c.push(t)}))};n.addEventListener("input",e)}let v=s.find((e=>o.value&&e.dataset.value===o.value));!v&&s.length>0&&(v=s[0]),d(v);const p=e=>{if(!["ArrowDown","ArrowUp","Enter","Home","End"].includes(e.key))return;if(!a.matches(":popover-open"))return void(e.currentTarget===t&&"Enter"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Enter"===e.key)return void(l>-1&&u(s[l]));if(0===c.length)return;const r=l>-1?c.indexOf(s[l]):-1;let i=r;switch(e.key){case"ArrowDown":r<c.length-1&&(i=r+1);break;case"ArrowUp":r>0?i=r-1:-1===r&&(i=0);break;case"Home":i=0;break;case"End":i=c.length-1}if(i!==r){r>-1&&c[r].classList.remove("active");const e=c[i];e.classList.add("active"),l=s.indexOf(e),e.id&&t.setAttribute("aria-activedescendant",e.id),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};t.addEventListener("keydown",p),n&&n.addEventListener("keydown",p),i.addEventListener("click",(e=>{const t=e.target.closest('[role="option"]');t&&u(t)})),a.addEventListener("toggle",(e=>{if(t.setAttribute("aria-expanded","open"===e.newState),"open"===e.newState){n&&n.focus();const e=i.querySelector('[role="option"][aria-selected="true"]');let r=null;e&&c.includes(e)?r=e:c.length>0&&(r=c[0]),l>-1&&s[l]?.classList.remove("active"),r?(l=s.indexOf(r),r.classList.add("active"),r.id&&t.setAttribute("aria-activedescendant",r.id),r.scrollIntoView({block:"nearest"})):l=-1}else"closed"===e.newState&&(n&&(n.value="",c=[...s],s.forEach((e=>e.setAttribute("aria-hidden","false")))),t.removeAttribute("aria-activedescendant"),l>-1&&s[l]?.classList.remove("active"),l=-1)})),e.dataset.selectInitialized=!0};document.querySelectorAll("div.select:not([data-select-initialized])").forEach(e);new MutationObserver((t=>{t.forEach((t=>{t.addedNodes.forEach((t=>{t.nodeType===Node.ELEMENT_NODE&&(t.matches("div.select:not([data-select-initialized])")&&e(t),t.querySelectorAll("div.select:not([data-select-initialized])").forEach(e))}))}))})).observe(document.body,{childList:!0,subtree:!0})})();
1
+ (()=>{const e=e=>{const t=e.querySelector(":scope > button"),r=t.querySelector(":scope > span"),a=e.querySelector(":scope > [data-popover]"),i=a.querySelector('[role="listbox"]'),s=e.querySelector(':scope > input[type="hidden"]'),n=e.querySelector('header input[type="text"]');if(!(t&&a&&i&&s))return;const o=Array.from(i.querySelectorAll('[role="option"]'));let c=[...o],d=-1;const l=e=>{e&&(r.innerHTML=e.dataset.label||e.innerHTML,s.value=e.dataset.value,i.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute("aria-selected"),e.setAttribute("aria-selected","true"))},u=()=>{a.setAttribute("aria-hidden","true"),t.setAttribute("aria-expanded","false"),n&&(n.value="",c=[...o],o.forEach((e=>e.setAttribute("aria-hidden","false")))),t.removeAttribute("aria-activedescendant"),d>-1&&o[d]?.classList.remove("active"),d=-1},v=e=>{e&&(e.dataset.value&&l(e),u())};if(n){const e=()=>{const e=n.value.trim().toLowerCase();d>-1&&(o[d].classList.remove("active"),t.removeAttribute("aria-activedescendant"),d=-1),c=[],o.forEach((t=>{const r=(t.dataset.label||t.textContent).trim().toLowerCase().includes(e);t.setAttribute("aria-hidden",String(!r)),r&&c.push(t)}))};n.addEventListener("input",e)}let b=o.find((e=>s.value&&e.dataset.value===s.value));!b&&o.length>0&&(b=o[0]),l(b);const p=e=>{const r="false"===a.getAttribute("aria-hidden");if(!["ArrowDown","ArrowUp","Enter","Home","End","Escape"].includes(e.key))return;if(!r)return void("Enter"!==e.key&&"Escape"!==e.key&&(e.preventDefault(),t.click()));if(e.preventDefault(),"Escape"===e.key)return void u();if("Enter"===e.key)return void(d>-1&&v(c[d]));if(0===c.length)return;const i=d>-1?c.indexOf(o[d]):-1;let s=i;switch(e.key){case"ArrowDown":i<c.length-1&&(s=i+1);break;case"ArrowUp":i>0?s=i-1:-1===i&&(s=0);break;case"Home":s=0;break;case"End":s=c.length-1}if(s!==i){i>-1&&c[i].classList.remove("active");const e=c[s];e.classList.add("active"),d=o.indexOf(e),e.id&&t.setAttribute("aria-activedescendant",e.id),e.scrollIntoView({block:"nearest",behavior:"smooth"})}};t.addEventListener("keydown",p),n&&n.addEventListener("keydown",p),t.addEventListener("click",(()=>{if("true"===t.getAttribute("aria-expanded"))u();else{a.setAttribute("aria-hidden","false"),t.setAttribute("aria-expanded","true"),n&&n.focus();const e=i.querySelector('[role="option"][aria-selected="true"]');e&&(d>-1&&o[d]?.classList.remove("active"),d=o.indexOf(e),e.classList.add("active"),e.id&&t.setAttribute("aria-activedescendant",e.id),e.scrollIntoView({block:"nearest"}))}})),i.addEventListener("click",(e=>{const t=e.target.closest('[role="option"]');t&&v(t)})),document.addEventListener("click",(t=>{e.contains(t.target)||u()})),a.setAttribute("aria-hidden","true"),e.dataset.selectInitialized=!0};document.querySelectorAll("div.select:not([data-select-initialized])").forEach(e);new MutationObserver((t=>{t.forEach((t=>{t.addedNodes.forEach((t=>{t.nodeType===Node.ELEMENT_NODE&&(t.matches("div.select:not([data-select-initialized])")&&e(t),t.querySelectorAll("div.select:not([data-select-initialized])").forEach(e))}))}))})).observe(document.body,{childList:!0,subtree:!0})})();
@@ -8,10 +8,10 @@
8
8
  @param footer {string} [optional] - HTML content for the dialog footer.
9
9
  @param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
10
10
  @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
11
- @param content_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
12
- @param content_header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
13
- @param content_body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
14
- @param content_footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
11
+ @param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content article.
12
+ @param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
13
+ @param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
14
+ @param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
15
15
  @param open {boolean} [optional] [default=false] - Whether the dialog should be open initially.
16
16
  @param close_button {boolean} [optional] [default=true] - Whether to include a close button.
17
17
  @param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
@@ -3,26 +3,31 @@
3
3
 
4
4
  @param id {string} [optional] - Unique identifier for the dropdown component.
5
5
  @param trigger {string} [optional] - HTML content for the button that triggers the dropdown.
6
- @param menu {array} [optional] - Array of menu items for the dropdown.
6
+ @param items {array} [optional] - Array of menu items for the dropdown.
7
7
  @param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
8
8
  @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
9
- @param content_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
9
+ @param popover_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
10
10
  #}
11
11
  {% macro dropdown_menu(
12
- id=None,
13
12
  trigger,
13
+ id=None,
14
14
  items=None,
15
+ main_attrs={},
15
16
  trigger_attrs={},
16
17
  popover_attrs={},
17
18
  menu_attrs={}
18
19
  ) %}
19
20
  {% set id = id or ("dropdown-menu-" + (range(100000, 999999) | random | string)) %}
20
21
 
21
- <div class="dropdown-menu">
22
+ <div
23
+ class="dropdown-menu {{ main_attrs.class }}"
24
+ {% for key, value in main_attrs %}
25
+ {% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
26
+ {% endfor %}
27
+ >
22
28
  <button
23
29
  type="button"
24
30
  id="{{ id }}-trigger"
25
- popovertarget="{{ id }}"
26
31
  aria-haspopup="menu"
27
32
  aria-controls="{{ id }}-menu"
28
33
  aria-expanded="false"
@@ -33,14 +38,14 @@
33
38
  {{ trigger | safe }}
34
39
  </button>
35
40
  <div
36
- popover
37
- class="popover p-1 {% if popover_attrs.class %} {{ popover_attrs.class }}{% endif %}"
38
41
  id="{{ id }}"
42
+ data-popover
43
+ aria-hidden="true"
39
44
  {% for key, value in popover_attrs %}
40
- {% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
45
+ {{ key }}="{{ value }}"
41
46
  {% endfor %}
42
47
  >
43
- <nav
48
+ <div
44
49
  role="menu"
45
50
  id="{{ id }}-menu"
46
51
  aria-labelledby="{{ id }}-trigger"
@@ -53,7 +58,7 @@
53
58
  {% else %}
54
59
  {{ caller() if caller }}
55
60
  {% endif %}
56
- </nav>
61
+ </div>
57
62
  </div>
58
63
  </div>
59
64
  {% endmacro %}
@@ -73,11 +78,13 @@
73
78
  <div
74
79
  role="group"
75
80
  aria-labelledby="{{ group_label_id }}"
76
- {% for key, value in item.attrs %}
77
- {{ key }}="{{ value }}"
78
- {% endfor %}
81
+ {% if item.attrs %}
82
+ {% for key, value in item.attrs %}
83
+ {{ key }}="{{ value }}"
84
+ {% endfor %}
85
+ {% endif %}
79
86
  >
80
- <div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
87
+ <div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
81
88
  {{ render_dropdown_items(item.items, item_id) if item.items }}
82
89
  </div>
83
90
  {% elif item.type == "separator" %}
@@ -85,24 +92,29 @@
85
92
  {% elif item.type == "item" or not item.type %}
86
93
  {% if item.url %}
87
94
  <a
95
+ id="{{ item_id }}"
88
96
  role="menuitem"
89
97
  href="{{ item.url }}"
90
- {% for key, value in item.attrs %}
91
- {% if key != "url" %} {{ key }}="{{ value }}" {% endif %}
92
- {% endfor %}
98
+ {% if item.attrs %}
99
+ {% for key, value in item.attrs %}
100
+ {% if key != "url" %} {{ key }}="{{ value }}" {% endif %}
101
+ {% endfor %}
102
+ {% endif %}
93
103
  >
94
104
  {{ item.label | safe }}
95
105
  </a>
96
106
  {% else %}
97
- <button
107
+ <div
108
+ id="{{ item_id }}"
98
109
  role="menuitem"
99
- type="button"
100
- {% for key, value in item.attrs %}
101
- {{ key }}="{{ value }}"
102
- {% endfor %}
110
+ {% if item.attrs %}
111
+ {% for key, value in item.attrs %}
112
+ {{ key }}="{{ value }}"
113
+ {% endfor %}
114
+ {% endif %}
103
115
  >
104
116
  {{ item.label | safe }}
105
- </button>
117
+ </div>
106
118
  {% endif %}
107
119
  {% endif %}
108
120
  {% endfor %}
@@ -5,35 +5,43 @@
5
5
  @param trigger {string} [optional] - HTML content for the element that triggers the popover.
6
6
  @param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
7
7
  @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger element.
8
- @param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
8
+ @param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
9
9
  #}
10
10
  {% macro popover(
11
- id=None,
12
11
  trigger,
12
+ id=None,
13
+ main_attrs={},
13
14
  trigger_attrs={},
14
15
  popover_attrs={}
15
16
  ) %}
16
17
  {% set id = id or ("popover-" + (range(100000, 999999) | random | string)) %}
17
- <button
18
- type="button"
19
- popovertarget="{{ id }}"
20
- id="{{ id }}-trigger"
21
- onclick="this.setAttribute('aria-expanded', this.getAttribute('aria-expanded') === 'false' ? 'true' : 'false')"
22
- aria-expanded="false"
23
- {% for key, value in trigger_attrs %}
24
- {{ key }}="{{ value }}"
25
- {% endfor %}
26
- >
27
- {{ trigger | safe }}
28
- </button>
18
+
29
19
  <div
30
- popover
31
- id="{{ id }}"
32
- class="popover {{ popover_attrs.class }}"
33
- {% for key, value in popover_attrs %}
20
+ class="popover {{ main_attrs.class }}"
21
+ {% for key, value in main_attrs %}
34
22
  {% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
35
23
  {% endfor %}
36
- >
37
- {{ caller() if caller }}
24
+ >
25
+ <button
26
+ id="{{ id }}-trigger"
27
+ type="button"
28
+ aria-expanded="false"
29
+ aria-controls="{{ id }}"
30
+ {% for key, value in trigger_attrs %}
31
+ {{ key }}="{{ value }}"
32
+ {% endfor %}
33
+ >
34
+ {{ trigger | safe }}
35
+ </button>
36
+ <div
37
+ id="{{ id }}"
38
+ data-popover
39
+ aria-hidden="true"
40
+ {% for key, value in popover_attrs %}
41
+ {{ key }}="{{ value }}"
42
+ {% endfor %}
43
+ >
44
+ {{ caller() if caller }}
45
+ </div>
38
46
  </div>
39
47
  {% endmacro %}
@@ -6,7 +6,7 @@
6
6
  @param name {string} [optional] - The name attribute for the hidden input storing the selected value.
7
7
  @param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
8
8
  @param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
9
- @param content_attrs {object} [optional] - Additional HTML attributes for the popover content div.
9
+ @param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
10
10
  @param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
11
11
  @param input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
12
12
  @param search_placeholder {string} [optional] [default="Search entries..."] - Placeholder text for the search input (combobox only).
@@ -17,9 +17,11 @@
17
17
  selected=None,
18
18
  name=None,
19
19
  items=None,
20
+ main_attrs={},
20
21
  trigger_attrs={},
21
22
  popover_attrs={},
22
23
  listbox_attrs={},
24
+ input_attrs={},
23
25
  search_placeholder="Search entries...",
24
26
  is_combobox=false
25
27
  ) %}
@@ -28,34 +30,40 @@
28
30
  {% set first_option = [] %}
29
31
  {% set selected_option = [] %}
30
32
 
31
- {% for item in items %}
32
- {% if item.type == "group" %}
33
- {% for sub_item in item.items %}
33
+ {% if items %}
34
+ {% for item in items %}
35
+ {% if item.type == "group" %}
36
+ {% for sub_item in item.items %}
37
+ {% if not first_option[0] %}
38
+ {% set first_option = (first_option.push(sub_item), first_option) %}
39
+ {% endif %}
40
+ {% if selected and sub_item.value == selected and not selected_option[0] %}
41
+ {% set selected_option = (selected_option.push(sub_item), selected_option) %}
42
+ {% endif %}
43
+ {% endfor %}
44
+ {% else %}
34
45
  {% if not first_option[0] %}
35
- {% set first_option = (first_option.push(sub_item), first_option) %}
46
+ {% set first_option = (first_option.push(item), first_option) %}
36
47
  {% endif %}
37
- {% if selected and sub_item.value == selected and not selected_option[0] %}
38
- {% set selected_option = (selected_option.push(sub_item), selected_option) %}
48
+ {% if selected and item.value == selected and not selected_option[0] %}
49
+ {% set selected_option = (selected_option.push(item), selected_option) %}
39
50
  {% endif %}
40
- {% endfor %}
41
- {% else %}
42
- {% if not first_option[0] %}
43
- {% set first_option = (first_option.push(item), first_option) %}
44
51
  {% endif %}
45
- {% if selected and item.value == selected and not selected_option[0] %}
46
- {% set selected_option = (selected_option.push(item), selected_option) %}
47
- {% endif %}
48
- {% endif %}
49
- {% endfor %}
52
+ {% endfor %}
53
+ {% endif %}
50
54
 
51
55
  {% set default_option = selected_option[0] or first_option[0] or None %}
52
56
 
53
- <div class="select">
57
+ <div
58
+ class="select {{ main_attrs.class }}"
59
+ {% for key, value in main_attrs %}
60
+ {% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
61
+ {% endfor %}
62
+ >
54
63
  <button
55
64
  type="button"
56
65
  class="btn-outline justify-between font-normal {{ trigger_attrs.class }}"
57
66
  id="{{ id }}-trigger"
58
- popovertarget="{{ id }}"
59
67
  aria-haspopup="listbox"
60
68
  aria-expanded="false"
61
69
  aria-controls="{{ id }}-listbox"
@@ -71,11 +79,11 @@
71
79
  {% endif %}
72
80
  </button>
73
81
  <div
74
- popover
75
82
  id="{{ id }}"
76
- class="popover {{ popover_attrs.class }}"
83
+ data-popover
84
+ aria-hidden="true"
77
85
  {% for key, value in popover_attrs %}
78
- {% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
86
+ {{ key }}="{{ value }}"
79
87
  {% endfor %}
80
88
  >
81
89
  {% if is_combobox %}
@@ -90,8 +98,8 @@
90
98
  spellcheck="false"
91
99
  aria-autocomplete="list"
92
100
  role="combobox"
93
- aria-expanded="true"
94
- aria-controls="{{ id }}-content"
101
+ aria-expanded="false"
102
+ aria-controls="{{ id }}-listbox"
95
103
  aria-labelledby="{{ id }}-trigger"
96
104
  >
97
105
  </header>
@@ -105,7 +113,7 @@
105
113
  {{ key }}="{{ value }}"
106
114
  {% endfor %}
107
115
  >
108
- {% if items.length > 0 %}
116
+ {% if items and items.length > 0 %}
109
117
  {{ render_select_items(items, default_option.value, id ~ "-items" if id else "items") }}
110
118
  {% else %}
111
119
  {{ caller() if caller }}
@@ -115,7 +123,7 @@
115
123
  <input
116
124
  type="hidden"
117
125
  name="{{ name or id ~ '-value' }}"
118
- value="{{ selected or '' }}"
126
+ value="{{ (default_option.value if default_option) or '' }}"
119
127
  {% for key, value in input_attrs %}
120
128
  {% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
121
129
  {% endfor %}
@@ -137,11 +145,13 @@
137
145
  <div
138
146
  role="group"
139
147
  aria-labelledby="{{ group_label_id }}"
140
- {% for key, value in item.attrs %}
141
- {{ key }}="{{ value }}"
142
- {% endfor %}
148
+ {% if item.attrs %}
149
+ {% for key, value in item.attrs %}
150
+ {{ key }}="{{ value }}"
151
+ {% endfor %}
152
+ {% endif %}
143
153
  >
144
- <div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
154
+ <div role="presentation" id="{{ group_label_id }}">{{ item.label }}</div>
145
155
  {{ render_select_items(item.items, selected, item_id) if item.items }}
146
156
  </div>
147
157
  {% elif item.type == "separator" %}
@@ -152,9 +162,11 @@
152
162
  role="option"
153
163
  data-value="{{ item.value }}"
154
164
  {% if selected == item.value %}aria-selected="true"{% endif %}
155
- {% for key, value in item.attrs %}
156
- {{ key }}="{{ value }}"
157
- {% endfor %}
165
+ {% if item.attrs %}
166
+ {% for key, value in item.attrs %}
167
+ {{ key }}="{{ value }}"
168
+ {% endfor %}
169
+ {% endif %}
158
170
  >
159
171
  {{ item.label | safe }}
160
172
  </div>