@stimulus-plumbers/controllers 0.3.3 → 0.4.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.
@@ -1,29 +1,59 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
  import { initCalendar } from '../plumbers';
3
+ import { attachCalendarDaySelector } from '../plumbers/calendar-selector';
3
4
  import { tryParseDate } from '../plumbers/plumber/date';
4
5
 
5
6
  export default class extends Controller {
6
7
  static targets = ['daysOfWeek', 'daysOfMonth'];
7
- static classes = ['dayOfWeek', 'dayOfMonth', 'week'];
8
+ static classes = ['dayOfWeek', 'dayOfMonth', 'dayOfOtherMonth', 'row'];
8
9
  static values = {
10
+ year: Number,
11
+ month: Number,
12
+ today: { type: String, default: '' },
13
+ selected: { type: String, default: '' },
14
+ since: { type: String, default: '' },
15
+ till: { type: String, default: '' },
9
16
  locales: { type: Array, default: ['default'] },
10
17
  weekdayFormat: { type: String, default: 'short' },
11
18
  dayFormat: { type: String, default: 'numeric' },
12
19
  daysOfOtherMonth: { type: Boolean, default: false },
13
- today: { type: String, default: '' },
14
- selected: { type: String, default: '' },
15
20
  };
16
21
 
17
22
  initialize() {
18
- initCalendar(this, { today: this.todayValue });
23
+ this.selector = attachCalendarDaySelector(this, { onSelect: 'select' });
24
+ initCalendar(this, {
25
+ today: this.todayValue,
26
+ since: this.sinceValue || null,
27
+ till: this.tillValue || null,
28
+ });
19
29
  }
20
30
 
21
31
  connect() {
22
- this.draw();
32
+ this.selector.attach();
33
+ this.navigated();
23
34
  }
24
35
 
25
- navigated() {
26
- this.draw();
36
+ disconnect() {
37
+ this.selector.detach();
38
+ }
39
+
40
+ yearValueChanged() {
41
+ if (!this.calendar || !this.hasYearValue) return;
42
+ this._scheduleNavigate();
43
+ }
44
+ monthValueChanged() {
45
+ if (!this.calendar || !this.hasYearValue) return;
46
+ this._scheduleNavigate();
47
+ }
48
+
49
+ _scheduleNavigate() {
50
+ if (this._navigatePending) return;
51
+ this._navigatePending = true;
52
+ queueMicrotask(async () => {
53
+ this._navigatePending = false;
54
+ await this.calendar.navigate(this.currentDate);
55
+ this.navigated();
56
+ });
27
57
  }
28
58
 
29
59
  selectedValueChanged() {
@@ -39,20 +69,38 @@ export default class extends Controller {
39
69
  if (!parsed) return;
40
70
 
41
71
  const time = this.daysOfMonthTarget.querySelector(`time[datetime="${parsed.toISOString()}"]`);
42
- if (time) time.closest('[aria-selected]').setAttribute('aria-selected', 'true');
72
+ if (time) time.closest('[aria-selected]')?.setAttribute('aria-selected', 'true');
73
+ }
74
+
75
+ navigate(date) {
76
+ this.yearValue = date.getFullYear();
77
+ this.monthValue = date.getMonth();
78
+ }
79
+
80
+ step(unit, dir) {
81
+ return this.calendar.step(unit, dir);
43
82
  }
44
83
 
45
- onSelect(event) {
46
- const iso = event.detail?.iso;
47
- if (iso) this.selectedValue = iso;
84
+ select(iso) {
85
+ const date = tryParseDate(iso);
86
+ if (!date) return;
87
+ this.selectedValue = iso;
88
+ if (date.getMonth() !== this.calendar.month || date.getFullYear() !== this.calendar.year) {
89
+ this.calendar.navigate(date);
90
+ }
91
+ this.dispatch('selected', { detail: { epoch: date.getTime(), iso } });
48
92
  }
49
93
 
50
- draw() {
94
+ navigated() {
51
95
  this.drawDaysOfWeek();
52
96
  this.drawDaysOfMonth();
53
97
  this.selectedValueChanged();
54
98
  }
55
99
 
100
+ get currentDate() {
101
+ return new Date(this.yearValue, this.monthValue, 1);
102
+ }
103
+
56
104
  createDayElement(day, { selectable = false, disabled = false } = {}) {
57
105
  const element = document.createElement(selectable ? 'button' : 'div');
58
106
  element.tabIndex = -1;
@@ -81,7 +129,7 @@ export default class extends Controller {
81
129
  }
82
130
  const row = document.createElement('div');
83
131
  row.setAttribute('role', 'row');
84
- if (this.hasWeekClass) row.classList.add(...this.weekClasses);
132
+ if (this.hasRowClass) row.classList.add(...this.rowClasses);
85
133
  row.replaceChildren(...daysOfWeek);
86
134
  this.daysOfWeekTarget.replaceChildren(row);
87
135
  }
@@ -93,17 +141,21 @@ export default class extends Controller {
93
141
  const today = new Date(t.getFullYear(), t.getMonth(), t.getDate()).getTime();
94
142
  const daysOfMonth = [];
95
143
  for (const date of this.calendar.daysOfMonth) {
96
- const dayDisabled =
97
- !date.current || this.calendar.isDisabled(date.date) || !this.calendar.isWithinRange(date.date);
144
+ const dayRuleDisabled = this.calendar.isDisabled(date.date) || !this.calendar.isWithinRange(date.date);
145
+ const dayOutsideNavigable = !date.current && this.daysOfOtherMonthValue && !dayRuleDisabled;
98
146
  const dayText = date.current || this.daysOfOtherMonthValue ? date.value : '';
99
147
  const dayElement = this.createDayElement(dayText, {
100
- selectable: date.current,
101
- disabled: dayDisabled,
148
+ selectable: date.current || dayOutsideNavigable,
149
+ disabled: date.current ? dayRuleDisabled : !dayOutsideNavigable,
102
150
  });
103
151
 
104
152
  if (today === date.date.getTime()) dayElement.setAttribute('aria-current', 'date');
105
153
  if (date.current || this.daysOfOtherMonthValue) dayElement.setAttribute('aria-selected', '');
106
- if (this.hasDayOfMonthClass) dayElement.classList.add(...this.dayOfMonthClasses);
154
+ if (!date.current && this.daysOfOtherMonthValue && this.hasDayOfOtherMonthClass) {
155
+ dayElement.classList.add(...this.dayOfOtherMonthClasses);
156
+ } else if (this.hasDayOfMonthClass) {
157
+ dayElement.classList.add(...this.dayOfMonthClasses);
158
+ }
107
159
 
108
160
  const time = document.createElement('time');
109
161
  time.dateTime = date.iso;
@@ -116,7 +168,7 @@ export default class extends Controller {
116
168
  for (let i = 0; i < daysOfMonth.length; i += 7) {
117
169
  const row = document.createElement('div');
118
170
  row.setAttribute('role', 'row');
119
- if (this.hasWeekClass) row.classList.add(...this.weekClasses);
171
+ if (this.hasRowClass) row.classList.add(...this.rowClasses);
120
172
  for (const day of daysOfMonth.slice(i, i + 7)) {
121
173
  day.setAttribute('role', 'gridcell');
122
174
  row.appendChild(day);
@@ -0,0 +1,14 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { attachCalendarDaySelector } from '../plumbers/calendar-selector';
3
+
4
+ export default class extends Controller {
5
+ initialize() {
6
+ this.selector = attachCalendarDaySelector(this);
7
+ }
8
+ connect() {
9
+ this.selector.attach();
10
+ }
11
+ disconnect() {
12
+ this.selector.detach();
13
+ }
14
+ }
@@ -0,0 +1,104 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { initCalendar } from '../plumbers';
3
+ import { attachCalendarMonthSelector } from '../plumbers/calendar-selector';
4
+ import { tryParseDate } from '../plumbers/plumber/date';
5
+
6
+ const MONTHS_PER_ROW = 4;
7
+
8
+ export default class extends Controller {
9
+ static targets = ['grid'];
10
+ static values = {
11
+ current: Number,
12
+ today: { type: String, default: '' },
13
+ selected: { type: String, default: '' },
14
+ since: { type: String, default: '' },
15
+ till: { type: String, default: '' },
16
+ locales: { type: Array, default: ['default'] },
17
+ monthFormat: { type: String, default: 'short' },
18
+ };
19
+
20
+ initialize() {
21
+ this.selector = attachCalendarMonthSelector(this);
22
+ initCalendar(this, {
23
+ today: this.todayValue,
24
+ year: this.currentValue || undefined,
25
+ since: this.sinceValue || null,
26
+ till: this.tillValue || null,
27
+ });
28
+ }
29
+
30
+ connect() {
31
+ this.selector.attach();
32
+ this.navigated();
33
+ }
34
+
35
+ disconnect() {
36
+ this.selector.detach();
37
+ }
38
+
39
+ currentValueChanged() {
40
+ if (!this.calendar || !this.hasCurrentValue) return;
41
+ this.calendar.navigate(this.currentDate);
42
+ }
43
+
44
+ selectedValueChanged() {
45
+ if (!this.hasGridTarget) return;
46
+ const selected = tryParseDate(this.selectedValue);
47
+ this.gridTarget.querySelectorAll('button[data-month]').forEach((btn) => {
48
+ const month = parseInt(btn.dataset.month, 10) - 1; // 0-indexed
49
+ btn.setAttribute('aria-selected', selected && selected.getMonth() === month ? 'true' : 'false');
50
+ });
51
+ }
52
+
53
+ navigate(date) {
54
+ this.currentValue = date.getFullYear();
55
+ }
56
+
57
+ step(unit, dir) {
58
+ return this.calendar.step(unit, dir);
59
+ }
60
+
61
+ navigated() {
62
+ this.drawGrid();
63
+ }
64
+
65
+ get currentDate() {
66
+ return new Date(this.currentValue, 0, 1);
67
+ }
68
+
69
+ drawGrid() {
70
+ if (!this.hasGridTarget) return;
71
+
72
+ const { year, monthsOfYear } = this.calendar;
73
+ const today = this.calendar.today;
74
+ const selectedDate = tryParseDate(this.selectedValue);
75
+ const cells = [];
76
+
77
+ const formatter = new Intl.DateTimeFormat(this.localesValue, { month: this.monthFormatValue });
78
+
79
+ for (const m of monthsOfYear) {
80
+ const btn = document.createElement('button');
81
+ btn.type = 'button';
82
+ btn.textContent = formatter.format(m.date);
83
+ btn.dataset.month = m.value + 1; // 1-indexed
84
+ btn.setAttribute('role', 'gridcell');
85
+ const isSelected = selectedDate && selectedDate.getMonth() === m.value;
86
+ btn.setAttribute('aria-selected', isSelected ? 'true' : 'false');
87
+ if (m.value === today.getMonth() && year === today.getFullYear()) {
88
+ btn.setAttribute('aria-current', 'month');
89
+ }
90
+ if (m.disabled) btn.setAttribute('aria-disabled', 'true');
91
+ cells.push(btn);
92
+ }
93
+
94
+ const rowgroup = document.createElement('div');
95
+ rowgroup.setAttribute('role', 'rowgroup');
96
+ for (let i = 0; i < cells.length; i += MONTHS_PER_ROW) {
97
+ const row = document.createElement('div');
98
+ row.setAttribute('role', 'row');
99
+ for (const cell of cells.slice(i, i + MONTHS_PER_ROW)) row.appendChild(cell);
100
+ rowgroup.appendChild(row);
101
+ }
102
+ this.gridTarget.replaceChildren(rowgroup);
103
+ }
104
+ }
@@ -0,0 +1,14 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { attachCalendarMonthSelector } from '../plumbers/calendar-selector';
3
+
4
+ export default class extends Controller {
5
+ initialize() {
6
+ this.selector = attachCalendarMonthSelector(this);
7
+ }
8
+ connect() {
9
+ this.selector.attach();
10
+ }
11
+ disconnect() {
12
+ this.selector.detach();
13
+ }
14
+ }
@@ -1,11 +1,14 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
  import { tryParseDate } from '../plumbers/plumber/date';
3
3
 
4
+ const VIEWS = ['month', 'year', 'decade'];
5
+
4
6
  export default class extends Controller {
5
- static targets = ['previous', 'next', 'day', 'month', 'year'];
6
- static outlets = ['calendar-month'];
7
+ static targets = ['previous', 'next', 'day', 'month', 'year', 'viewTitle'];
8
+ static outlets = ['calendar-month', 'calendar-year', 'calendar-decade'];
7
9
  static values = {
8
10
  date: String,
11
+ view: { type: String, default: 'month' },
9
12
  locales: { type: Array, default: ['default'] },
10
13
  dayFormat: { type: String, default: 'numeric' },
11
14
  monthFormat: { type: String, default: 'long' },
@@ -25,33 +28,66 @@ export default class extends Controller {
25
28
  this.draw();
26
29
  }
27
30
 
28
- onSelect(event) {
31
+ onDaySelect(event) {
29
32
  this.dateValue = event.detail.iso;
30
33
  this.draw();
31
34
  this.dispatch('selected', { detail: { value: event.detail.iso }, bubbles: true });
32
35
  }
33
36
 
37
+ zoomOut() {
38
+ const idx = VIEWS.indexOf(this.viewValue);
39
+ if (idx < VIEWS.length - 1) {
40
+ this.viewValue = VIEWS[idx + 1];
41
+ this.draw();
42
+ }
43
+ }
44
+
45
+ async onMonthSelect(event) {
46
+ const { month } = event.detail; // 1-indexed
47
+ if (!this.hasCalendarMonthOutlet) return;
48
+ const { year } = this.calendarMonthOutlet.calendar;
49
+ await this.calendarMonthOutlet.calendar.navigate(new Date(year, month - 1, 1));
50
+ this.viewValue = 'month';
51
+ this.draw();
52
+ }
53
+
54
+ async onYearSelect(event) {
55
+ const { year } = event.detail;
56
+ if (!this.hasCalendarMonthOutlet) return;
57
+ const { month } = this.calendarMonthOutlet.calendar;
58
+ await this.calendarMonthOutlet.calendar.navigate(new Date(year, month, 1));
59
+ if (this.hasCalendarYearOutlet) {
60
+ this.calendarYearOutlet.navigate(this.calendarMonthOutlet.calendar.current);
61
+ }
62
+ this.viewValue = 'year';
63
+ this.draw();
64
+ }
65
+
34
66
  previousTargetConnected(target) {
35
67
  target.addEventListener('click', this.previous);
36
68
  }
69
+
37
70
  previousTargetDisconnected(target) {
38
71
  target.removeEventListener('click', this.previous);
39
72
  }
40
73
 
41
74
  async previous() {
42
- await this.calendarMonthOutlet.calendar.step('month', -1);
75
+ await this.calendarMonthOutlet.calendar.step(...this.stepArgs(-1));
76
+ this.syncOutletValues();
43
77
  this.draw();
44
78
  }
45
79
 
46
80
  nextTargetConnected(target) {
47
81
  target.addEventListener('click', this.next);
48
82
  }
83
+
49
84
  nextTargetDisconnected(target) {
50
85
  target.removeEventListener('click', this.next);
51
86
  }
52
87
 
53
88
  async next() {
54
- await this.calendarMonthOutlet.calendar.step('month', 1);
89
+ await this.calendarMonthOutlet.calendar.step(...this.stepArgs(1));
90
+ this.syncOutletValues();
55
91
  this.draw();
56
92
  }
57
93
 
@@ -59,6 +95,8 @@ export default class extends Controller {
59
95
  this.drawDay();
60
96
  this.drawMonth();
61
97
  this.drawYear();
98
+ this.drawViewTitle();
99
+ this.drawView();
62
100
  }
63
101
 
64
102
  drawDay() {
@@ -84,4 +122,51 @@ export default class extends Controller {
84
122
  new Date(year, 0)
85
123
  );
86
124
  }
125
+
126
+ drawViewTitle() {
127
+ if (!this.hasViewTitleTarget || !this.hasCalendarMonthOutlet) return;
128
+ const { year, month } = this.calendarMonthOutlet.calendar;
129
+ this.viewTitleTarget.textContent = this.viewTitleLabel(year, month);
130
+ }
131
+
132
+ syncOutletValues() {
133
+ if (!this.hasCalendarMonthOutlet) return;
134
+ const { current } = this.calendarMonthOutlet.calendar;
135
+ if (!current) return;
136
+ if (this.hasCalendarYearOutlet) this.calendarYearOutlet.navigate(current);
137
+ if (this.hasCalendarDecadeOutlet) this.calendarDecadeOutlet.navigate(current);
138
+ }
139
+
140
+ stepArgs(direction) {
141
+ if (this.viewValue === 'year') return ['year', direction];
142
+ if (this.viewValue === 'decade') return ['year', direction * 10];
143
+ return ['month', direction];
144
+ }
145
+
146
+ viewTitleLabel(year, month) {
147
+ if (this.viewValue === 'year') return String(year);
148
+ if (this.viewValue === 'decade') {
149
+ const start = Math.floor(year / 10) * 10;
150
+ return `${start}–${start + 9}`;
151
+ }
152
+ return new Intl.DateTimeFormat(this.localesValue, { month: 'long', year: 'numeric' }).format(new Date(year, month));
153
+ }
154
+
155
+ drawView() {
156
+ const inMonthView = this.viewValue === 'month';
157
+ const inYearView = this.viewValue === 'year';
158
+ const inDecadeView = this.viewValue === 'decade';
159
+
160
+ if (this.hasCalendarMonthOutlet) {
161
+ const outlet = this.calendarMonthOutlet;
162
+ if (outlet.hasDaysOfWeekTarget) outlet.daysOfWeekTarget.hidden = !inMonthView;
163
+ if (outlet.hasDaysOfMonthTarget) outlet.daysOfMonthTarget.hidden = !inMonthView;
164
+ }
165
+ if (this.hasCalendarYearOutlet) {
166
+ this.calendarYearOutletElement.hidden = !inYearView;
167
+ }
168
+ if (this.hasCalendarDecadeOutlet) {
169
+ this.calendarDecadeOutletElement.hidden = !inDecadeView;
170
+ }
171
+ }
87
172
  }
@@ -1,6 +1,7 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
  import { Requestor } from '../requestor';
3
3
  import { filterOptions } from '../researcher';
4
+ import { ListboxNavigation } from '../accessibility/keyboard';
4
5
 
5
6
  export default class extends Controller {
6
7
  static targets = ['listbox', 'loading', 'empty'];
@@ -14,6 +15,16 @@ export default class extends Controller {
14
15
  this._requestor = new Requestor();
15
16
  }
16
17
 
18
+ connect() {
19
+ if (this.hasListboxTarget) {
20
+ this.listboxNav = new ListboxNavigation(this.listboxTarget);
21
+ }
22
+ }
23
+
24
+ disconnect() {
25
+ this._requestor.cancel();
26
+ }
27
+
17
28
  onSelect(event) {
18
29
  const option = event.target.closest('[role="option"]');
19
30
  if (!option || option.getAttribute('aria-disabled') === 'true') return;
@@ -29,27 +40,7 @@ export default class extends Controller {
29
40
  }
30
41
 
31
42
  onNavigate(event) {
32
- if (!['ArrowUp', 'ArrowDown', 'Enter', ' '].includes(event.key)) return;
33
- event.preventDefault();
34
- if (event.key === 'Enter' || event.key === ' ') {
35
- this.listboxTarget.querySelector('[aria-selected="true"]')?.click();
36
- return;
37
- }
38
- this.step(event.key === 'ArrowDown' ? 1 : -1);
39
- }
40
-
41
- step(delta) {
42
- const options = [
43
- ...this.listboxTarget.querySelectorAll('[role="option"]:not([aria-disabled="true"]):not([hidden])'),
44
- ];
45
- if (!options.length) return;
46
- const current = this.listboxTarget.querySelector('[aria-selected="true"]');
47
- const idx = options.indexOf(current);
48
- const next = delta > 0 ? options[Math.min(idx + 1, options.length - 1)] : options[Math.max(idx - 1, 0)];
49
- if (!next || next === current) return;
50
- options.forEach((o) => o.setAttribute('aria-selected', 'false'));
51
- next.setAttribute('aria-selected', 'true');
52
- next.scrollIntoView({ block: 'nearest' });
43
+ this.listboxNav?.handleKeyDown(event);
53
44
  }
54
45
 
55
46
  filter(query) {
@@ -90,8 +81,4 @@ export default class extends Controller {
90
81
  setEmpty(on) {
91
82
  if (this.hasEmptyTarget) this.emptyTarget.hidden = !on;
92
83
  }
93
-
94
- disconnect() {
95
- this._requestor.cancel();
96
- }
97
84
  }
@@ -1,55 +1,15 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
- import { focusFirst } from '../accessibility/focus';
3
- import { attachDismisser, attachVisibility } from '../plumbers';
4
2
 
5
3
  export default class extends Controller {
6
- static targets = ['trigger', 'popover', 'input'];
4
+ static targets = ['trigger', 'input'];
7
5
  static values = {
8
6
  value: String,
9
7
  minLength: { type: Number, default: 1 },
10
8
  };
11
9
  static outlets = ['combobox-dropdown'];
12
10
 
13
- connect() {
14
- attachDismisser(this);
15
- if (this.hasPopoverTarget) {
16
- attachVisibility(this, {
17
- element: this.popoverTarget,
18
- activator: this.hasTriggerTarget ? this.triggerTarget : null,
19
- });
20
- }
21
- }
22
-
23
- async dismissed() {
24
- await this.close();
25
- }
26
-
27
- async open() {
28
- if (!this.hasPopoverTarget) return;
29
- await this.visibility.show();
30
- }
31
-
32
- async close() {
33
- if (!this.hasPopoverTarget) return;
34
- await this.visibility.hide();
35
- }
36
-
37
- async toggle() {
38
- this.visibility?.visible ? await this.close() : await this.open();
39
- }
40
-
41
- async shown() {
42
- if (this.hasPopoverTarget) focusFirst(this.popoverTarget);
43
- }
44
-
45
- async hidden() {
46
- if (this.hasTriggerTarget) this.triggerTarget.focus();
47
- }
48
-
49
- // Receives combobox-*:selected events from sub-controllers
50
- async onSelect(event) {
11
+ onSelect(event) {
51
12
  if (event.detail?.value !== undefined) this.valueValue = event.detail.value;
52
- await this.close();
53
13
  }
54
14
 
55
15
  onInput(event) {
@@ -22,7 +22,10 @@ export default class extends Controller {
22
22
  modal.addEventListener('cancel', this.onCancel);
23
23
  modal.addEventListener('click', this.onBackdropClick);
24
24
  } else {
25
- this.focusTrap = new FocusTrap(modal, { escapeDeactivates: true });
25
+ this.focusTrap = new FocusTrap(modal, {
26
+ escapeDeactivates: true,
27
+ onDeactivate: () => this.close(),
28
+ });
26
29
  attachDismisser(this, { element: modal });
27
30
  }
28
31
  }
@@ -1,47 +1,74 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
- import { attachContentLoader, attachVisibility } from '../plumbers';
2
+ import { focusFirst } from '../accessibility/focus';
3
+ import { announce } from '../accessibility/aria';
4
+ import { attachContentLoader, attachDismisser, attachVisibility } from '../plumbers';
3
5
 
4
6
  export default class extends Controller {
5
- static targets = ['content', 'template', 'loader', 'activator'];
7
+ static targets = ['trigger', 'panel', 'template', 'loader'];
6
8
  static classes = ['hidden'];
7
9
  static values = {
8
10
  url: String,
9
11
  loadedAt: String,
10
12
  reload: { type: String, default: 'never' },
11
13
  staleAfter: { type: Number, default: 3600 },
14
+ closeOnSelect: { type: Boolean, default: true },
15
+ announceOpen: { type: String, default: 'Panel opened' },
16
+ announceClose: { type: String, default: 'Panel closed' },
12
17
  };
13
18
 
14
19
  connect() {
15
20
  attachContentLoader(this, {
16
- element: this.hasContentTarget ? this.contentTarget : null,
21
+ element: this.hasPanelTarget ? this.panelTarget : null,
17
22
  url: this.hasUrlValue ? this.urlValue : null,
18
23
  });
19
24
 
20
- if (this.hasContentTarget) {
25
+ if (this.hasPanelTarget) {
21
26
  attachVisibility(this, {
22
- element: this.contentTarget,
23
- activator: this.hasActivatorTarget ? this.activatorTarget : null,
27
+ element: this.panelTarget,
28
+ activator: this.hasTriggerTarget ? this.triggerTarget : null,
24
29
  });
30
+ attachDismisser(this);
25
31
  }
26
32
  if (this.hasLoaderTarget)
27
33
  attachVisibility(this, { element: this.loaderTarget, visibility: 'contentLoaderVisibility' });
28
34
  }
29
35
 
30
- async show() {
36
+ async dismissed() {
37
+ await this.close();
38
+ }
39
+
40
+ async open() {
41
+ if (!this.hasPanelTarget) return;
31
42
  await this.visibility.show();
32
43
  }
33
44
 
34
- async hide() {
45
+ async close() {
46
+ if (!this.hasPanelTarget) return;
35
47
  await this.visibility.hide();
36
48
  }
37
49
 
50
+ async toggle() {
51
+ this.visibility?.visible ? await this.close() : await this.open();
52
+ }
53
+
54
+ async closeOnSelect() {
55
+ if (this.closeOnSelectValue) await this.close();
56
+ }
57
+
38
58
  async shown() {
39
59
  await this.load();
60
+ if (this.hasPanelTarget) focusFirst(this.panelTarget);
61
+ announce(this.announceOpenValue);
62
+ }
63
+
64
+ async hidden() {
65
+ if (this.hasTriggerTarget) this.triggerTarget.focus();
66
+ announce(this.announceCloseValue);
40
67
  }
41
68
 
42
69
  canLoad() {
43
- if (this.hasContentTarget && this.contentTarget.tagName.toLowerCase() === 'turbo-frame') {
44
- if (this.hasUrlValue) this.contentTarget.setAttribute('src', this.urlValue);
70
+ if (this.hasPanelTarget && this.panelTarget.tagName.toLowerCase() === 'turbo-frame') {
71
+ if (this.hasUrlValue) this.panelTarget.setAttribute('src', this.urlValue);
45
72
  return false;
46
73
  }
47
74
  return true;
@@ -52,8 +79,8 @@ export default class extends Controller {
52
79
  }
53
80
 
54
81
  async contentLoaded({ content }) {
55
- if (this.hasContentTarget) {
56
- this.contentTarget.replaceChildren(this.getContentNode(content));
82
+ if (this.hasPanelTarget) {
83
+ this.panelTarget.replaceChildren(this.getContentNode(content));
57
84
  }
58
85
  if (this.hasLoaderTarget) await this.contentLoaderVisibility.hide();
59
86
  }