@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.
- package/README.md +31 -8
- package/dist/controllers.manifest.json +386 -0
- package/dist/stimulus-plumbers-controllers.es.js +756 -304
- package/dist/stimulus-plumbers-controllers.umd.js +1 -1
- package/package.json +3 -4
- package/src/accessibility/aria.js +6 -0
- package/src/accessibility/focus.js +4 -20
- package/src/accessibility/keyboard.js +134 -28
- package/src/controllers/calendar_decade_controller.js +99 -0
- package/src/controllers/calendar_decade_selector_controller.js +14 -0
- package/src/controllers/calendar_month_controller.js +71 -19
- package/src/controllers/calendar_month_selector_controller.js +14 -0
- package/src/controllers/calendar_year_controller.js +104 -0
- package/src/controllers/calendar_year_selector_controller.js +14 -0
- package/src/controllers/combobox_date_controller.js +90 -5
- package/src/controllers/combobox_dropdown_controller.js +12 -25
- package/src/controllers/input_combobox_controller.js +2 -42
- package/src/controllers/modal_controller.js +4 -1
- package/src/controllers/popover_controller.js +39 -12
- package/src/controllers/timeline_controller.js +71 -0
- package/src/index.js +6 -1
- package/src/plumbers/calendar-selector.js +85 -0
- package/src/plumbers/calendar.js +44 -10
- package/src/plumbers/index.js +5 -0
- package/src/plumbers/visibility.js +3 -3
- package/src/controllers/calendar_month_observer_controller.js +0 -27
|
@@ -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', '
|
|
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
|
-
|
|
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.
|
|
32
|
+
this.selector.attach();
|
|
33
|
+
this.navigated();
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
this.
|
|
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]')
|
|
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
|
-
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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
|
|
97
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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', '
|
|
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
|
-
|
|
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, {
|
|
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 {
|
|
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 = ['
|
|
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.
|
|
21
|
+
element: this.hasPanelTarget ? this.panelTarget : null,
|
|
17
22
|
url: this.hasUrlValue ? this.urlValue : null,
|
|
18
23
|
});
|
|
19
24
|
|
|
20
|
-
if (this.
|
|
25
|
+
if (this.hasPanelTarget) {
|
|
21
26
|
attachVisibility(this, {
|
|
22
|
-
element: this.
|
|
23
|
-
activator: this.
|
|
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
|
|
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
|
|
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.
|
|
44
|
-
if (this.hasUrlValue) this.
|
|
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.
|
|
56
|
-
this.
|
|
82
|
+
if (this.hasPanelTarget) {
|
|
83
|
+
this.panelTarget.replaceChildren(this.getContentNode(content));
|
|
57
84
|
}
|
|
58
85
|
if (this.hasLoaderTarget) await this.contentLoaderVisibility.hide();
|
|
59
86
|
}
|