@stimulus-plumbers/controllers 0.4.0 → 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 +125 -12
- package/dist/stimulus-plumbers-controllers.es.js +681 -312
- package/dist/stimulus-plumbers-controllers.umd.js +1 -1
- package/package.json +1 -1
- 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 +69 -17
- 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 +36 -85
- package/src/controllers/combobox_dropdown_controller.js +12 -25
- package/src/controllers/input_combobox_controller.js +0 -1
- package/src/controllers/modal_controller.js +4 -1
- package/src/controllers/popover_controller.js +5 -0
- 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 +25 -13
- package/src/plumbers/index.js +5 -0
- package/src/plumbers/visibility.js +3 -3
- package/src/controllers/calendar_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', 'row'];
|
|
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;
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
|
@@ -4,8 +4,8 @@ import { tryParseDate } from '../plumbers/plumber/date';
|
|
|
4
4
|
const VIEWS = ['month', 'year', 'decade'];
|
|
5
5
|
|
|
6
6
|
export default class extends Controller {
|
|
7
|
-
static targets = ['previous', 'next', 'day', 'month', 'year', 'viewTitle'
|
|
8
|
-
static outlets = ['calendar-month'];
|
|
7
|
+
static targets = ['previous', 'next', 'day', 'month', 'year', 'viewTitle'];
|
|
8
|
+
static outlets = ['calendar-month', 'calendar-year', 'calendar-decade'];
|
|
9
9
|
static values = {
|
|
10
10
|
date: String,
|
|
11
11
|
view: { type: String, default: 'month' },
|
|
@@ -18,8 +18,6 @@ export default class extends Controller {
|
|
|
18
18
|
initialize() {
|
|
19
19
|
this.previous = this.previous.bind(this);
|
|
20
20
|
this.next = this.next.bind(this);
|
|
21
|
-
this.selectMonth = this.selectMonth.bind(this);
|
|
22
|
-
this.selectYear = this.selectYear.bind(this);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
async calendarMonthOutletConnected() {
|
|
@@ -30,7 +28,7 @@ export default class extends Controller {
|
|
|
30
28
|
this.draw();
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
onDaySelect(event) {
|
|
34
32
|
this.dateValue = event.detail.iso;
|
|
35
33
|
this.draw();
|
|
36
34
|
this.dispatch('selected', { detail: { value: event.detail.iso }, bubbles: true });
|
|
@@ -44,22 +42,23 @@ export default class extends Controller {
|
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
async
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
const month = parseInt(btn.dataset.month, 10) - 1; // data-month is 1-indexed
|
|
45
|
+
async onMonthSelect(event) {
|
|
46
|
+
const { month } = event.detail; // 1-indexed
|
|
47
|
+
if (!this.hasCalendarMonthOutlet) return;
|
|
51
48
|
const { year } = this.calendarMonthOutlet.calendar;
|
|
52
|
-
await this.calendarMonthOutlet.calendar.navigate(new Date(year, month, 1));
|
|
49
|
+
await this.calendarMonthOutlet.calendar.navigate(new Date(year, month - 1, 1));
|
|
53
50
|
this.viewValue = 'month';
|
|
54
51
|
this.draw();
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
async
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
const year = parseInt(btn.dataset.year, 10);
|
|
54
|
+
async onYearSelect(event) {
|
|
55
|
+
const { year } = event.detail;
|
|
56
|
+
if (!this.hasCalendarMonthOutlet) return;
|
|
61
57
|
const { month } = this.calendarMonthOutlet.calendar;
|
|
62
58
|
await this.calendarMonthOutlet.calendar.navigate(new Date(year, month, 1));
|
|
59
|
+
if (this.hasCalendarYearOutlet) {
|
|
60
|
+
this.calendarYearOutlet.navigate(this.calendarMonthOutlet.calendar.current);
|
|
61
|
+
}
|
|
63
62
|
this.viewValue = 'year';
|
|
64
63
|
this.draw();
|
|
65
64
|
}
|
|
@@ -74,6 +73,7 @@ export default class extends Controller {
|
|
|
74
73
|
|
|
75
74
|
async previous() {
|
|
76
75
|
await this.calendarMonthOutlet.calendar.step(...this.stepArgs(-1));
|
|
76
|
+
this.syncOutletValues();
|
|
77
77
|
this.draw();
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -87,32 +87,15 @@ export default class extends Controller {
|
|
|
87
87
|
|
|
88
88
|
async next() {
|
|
89
89
|
await this.calendarMonthOutlet.calendar.step(...this.stepArgs(1));
|
|
90
|
+
this.syncOutletValues();
|
|
90
91
|
this.draw();
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
yearViewTargetConnected(target) {
|
|
94
|
-
target.addEventListener('click', this.selectMonth);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
yearViewTargetDisconnected(target) {
|
|
98
|
-
target.removeEventListener('click', this.selectMonth);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
decadeViewTargetConnected(target) {
|
|
102
|
-
target.addEventListener('click', this.selectYear);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
decadeViewTargetDisconnected(target) {
|
|
106
|
-
target.removeEventListener('click', this.selectYear);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
94
|
draw() {
|
|
110
95
|
this.drawDay();
|
|
111
96
|
this.drawMonth();
|
|
112
97
|
this.drawYear();
|
|
113
98
|
this.drawViewTitle();
|
|
114
|
-
this.drawYearView();
|
|
115
|
-
this.drawDecadeView();
|
|
116
99
|
this.drawView();
|
|
117
100
|
}
|
|
118
101
|
|
|
@@ -146,49 +129,18 @@ export default class extends Controller {
|
|
|
146
129
|
this.viewTitleTarget.textContent = this.viewTitleLabel(year, month);
|
|
147
130
|
}
|
|
148
131
|
|
|
149
|
-
|
|
150
|
-
if (!this.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const m of monthsOfYear) {
|
|
157
|
-
const btn = document.createElement('button');
|
|
158
|
-
btn.type = 'button';
|
|
159
|
-
btn.textContent = m.short;
|
|
160
|
-
btn.dataset.month = m.value + 1; // 1-indexed to match SSR convention
|
|
161
|
-
btn.setAttribute('role', 'gridcell');
|
|
162
|
-
btn.setAttribute('aria-selected', m.value === month ? 'true' : 'false');
|
|
163
|
-
if (m.value === today.getMonth() && year === today.getFullYear()) {
|
|
164
|
-
btn.setAttribute('aria-current', 'month');
|
|
165
|
-
}
|
|
166
|
-
cells.push(btn);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
this.yearViewTarget.replaceChildren(...cells);
|
|
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);
|
|
170
138
|
}
|
|
171
139
|
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const todayYear = this.calendarMonthOutlet.calendar.today.getFullYear();
|
|
177
|
-
const cells = [];
|
|
178
|
-
|
|
179
|
-
for (const y of yearsOfDecade) {
|
|
180
|
-
const btn = document.createElement('button');
|
|
181
|
-
btn.type = 'button';
|
|
182
|
-
btn.textContent = y.value;
|
|
183
|
-
btn.dataset.year = y.value;
|
|
184
|
-
btn.setAttribute('role', 'gridcell');
|
|
185
|
-
btn.setAttribute('aria-selected', y.value === year ? 'true' : 'false');
|
|
186
|
-
if (y.value === todayYear) btn.setAttribute('aria-current', 'year');
|
|
187
|
-
if (y.outside) btn.setAttribute('aria-disabled', 'true');
|
|
188
|
-
cells.push(btn);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
this.decadeViewTarget.replaceChildren(...cells);
|
|
140
|
+
stepArgs(direction) {
|
|
141
|
+
if (this.viewValue === 'year') return ['year', direction];
|
|
142
|
+
if (this.viewValue === 'decade') return ['year', direction * 10];
|
|
143
|
+
return ['month', direction];
|
|
192
144
|
}
|
|
193
145
|
|
|
194
146
|
viewTitleLabel(year, month) {
|
|
@@ -200,22 +152,21 @@ export default class extends Controller {
|
|
|
200
152
|
return new Intl.DateTimeFormat(this.localesValue, { month: 'long', year: 'numeric' }).format(new Date(year, month));
|
|
201
153
|
}
|
|
202
154
|
|
|
203
|
-
stepArgs(direction) {
|
|
204
|
-
if (this.viewValue === 'year') return ['year', direction];
|
|
205
|
-
if (this.viewValue === 'decade') return ['year', direction * 10];
|
|
206
|
-
return ['month', direction];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
155
|
drawView() {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
156
|
+
const inMonthView = this.viewValue === 'month';
|
|
157
|
+
const inYearView = this.viewValue === 'year';
|
|
158
|
+
const inDecadeView = this.viewValue === 'decade';
|
|
213
159
|
|
|
214
160
|
if (this.hasCalendarMonthOutlet) {
|
|
215
161
|
const outlet = this.calendarMonthOutlet;
|
|
216
|
-
|
|
217
|
-
if (outlet.
|
|
218
|
-
|
|
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;
|
|
219
170
|
}
|
|
220
171
|
}
|
|
221
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
|
}
|
|
@@ -8,7 +8,6 @@ export default class extends Controller {
|
|
|
8
8
|
};
|
|
9
9
|
static outlets = ['combobox-dropdown'];
|
|
10
10
|
|
|
11
|
-
// Commits the value only; popover#closeOnSelect handles dismissal.
|
|
12
11
|
onSelect(event) {
|
|
13
12
|
if (event.detail?.value !== undefined) this.valueValue = event.detail.value;
|
|
14
13
|
}
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
import { focusFirst } from '../accessibility/focus';
|
|
3
|
+
import { announce } from '../accessibility/aria';
|
|
3
4
|
import { attachContentLoader, attachDismisser, attachVisibility } from '../plumbers';
|
|
4
5
|
|
|
5
6
|
export default class extends Controller {
|
|
@@ -11,6 +12,8 @@ export default class extends Controller {
|
|
|
11
12
|
reload: { type: String, default: 'never' },
|
|
12
13
|
staleAfter: { type: Number, default: 3600 },
|
|
13
14
|
closeOnSelect: { type: Boolean, default: true },
|
|
15
|
+
announceOpen: { type: String, default: 'Panel opened' },
|
|
16
|
+
announceClose: { type: String, default: 'Panel closed' },
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
connect() {
|
|
@@ -55,10 +58,12 @@ export default class extends Controller {
|
|
|
55
58
|
async shown() {
|
|
56
59
|
await this.load();
|
|
57
60
|
if (this.hasPanelTarget) focusFirst(this.panelTarget);
|
|
61
|
+
announce(this.announceOpenValue);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
async hidden() {
|
|
61
65
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
66
|
+
announce(this.announceCloseValue);
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
canLoad() {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { DateFormatter } from '../plumbers/formatters/date';
|
|
3
|
+
import { RovingTabIndex } from '../accessibility/keyboard';
|
|
4
|
+
import { setExpanded, setHidden, announce } from '../accessibility/aria';
|
|
5
|
+
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static targets = ['trigger', 'detail', 'time'];
|
|
8
|
+
static values = {
|
|
9
|
+
dateFormat: { type: Object, default: {} },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this.rovingTabIndex = new RovingTabIndex(this.triggerTargets, { orientation: 'vertical' });
|
|
14
|
+
this.rovingTabIndex.activate();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
this.rovingTabIndex?.deactivate();
|
|
19
|
+
this.rovingTabIndex = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toggle(event) {
|
|
23
|
+
const trigger = event.currentTarget;
|
|
24
|
+
trigger.getAttribute('aria-expanded') === 'true' ? this.collapseItem(trigger) : this.expandItem(trigger);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
expand(event) {
|
|
28
|
+
this.expandItem(event.currentTarget);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
collapse(event) {
|
|
32
|
+
this.collapseItem(event.currentTarget);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
expandItem(trigger) {
|
|
36
|
+
const detail = this.detailTargets.find((d) => d.id === trigger.getAttribute('aria-controls')) ?? null;
|
|
37
|
+
this.dispatch('expand', { detail: { trigger, detail } });
|
|
38
|
+
setExpanded(trigger, true);
|
|
39
|
+
if (detail) {
|
|
40
|
+
setHidden(detail, false);
|
|
41
|
+
announce(`${trigger.textContent.trim()} expanded`);
|
|
42
|
+
}
|
|
43
|
+
this.dispatch('expanded', { detail: { trigger, detail } });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
collapseItem(trigger) {
|
|
47
|
+
const detail = this.detailTargets.find((d) => d.id === trigger.getAttribute('aria-controls')) ?? null;
|
|
48
|
+
this.dispatch('collapse', { detail: { trigger, detail } });
|
|
49
|
+
setExpanded(trigger, false);
|
|
50
|
+
if (detail) {
|
|
51
|
+
setHidden(detail, true);
|
|
52
|
+
announce(`${trigger.textContent.trim()} collapsed`);
|
|
53
|
+
}
|
|
54
|
+
this.dispatch('collapsed', { detail: { trigger, detail } });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
triggerTargetConnected(trigger) {
|
|
58
|
+
if (!trigger.hasAttribute('aria-expanded')) trigger.setAttribute('aria-expanded', 'false');
|
|
59
|
+
this.rovingTabIndex?.updateItems(this.triggerTargets);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
triggerTargetDisconnected() {
|
|
63
|
+
this.rovingTabIndex?.updateItems(this.triggerTargets);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
timeTargetConnected(el) {
|
|
67
|
+
if (!Object.keys(this.dateFormatValue).length || el.textContent.trim()) return;
|
|
68
|
+
const formatted = DateFormatter.format(el.getAttribute('datetime'), this.dateFormatValue);
|
|
69
|
+
if (formatted) el.textContent = formatted;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.js
CHANGED
|
@@ -17,8 +17,12 @@ export { fuzzyMatcher, filterOptions } from './researcher.js';
|
|
|
17
17
|
export { Formatter, FORMATTER_TYPES } from './plumbers/formatter.js';
|
|
18
18
|
|
|
19
19
|
// Export Stimulus controllers
|
|
20
|
+
export { default as CalendarDecadeController } from './controllers/calendar_decade_controller.js';
|
|
21
|
+
export { default as CalendarDecadeSelectorController } from './controllers/calendar_decade_selector_controller.js';
|
|
20
22
|
export { default as CalendarMonthController } from './controllers/calendar_month_controller.js';
|
|
21
|
-
export { default as
|
|
23
|
+
export { default as CalendarMonthSelectorController } from './controllers/calendar_month_selector_controller.js';
|
|
24
|
+
export { default as CalendarYearController } from './controllers/calendar_year_controller.js';
|
|
25
|
+
export { default as CalendarYearSelectorController } from './controllers/calendar_year_selector_controller.js';
|
|
22
26
|
export { default as ClipboardController } from './controllers/clipboard_controller.js';
|
|
23
27
|
export { default as ComboboxDateController } from './controllers/combobox_date_controller.js';
|
|
24
28
|
export { default as ComboboxDropdownController } from './controllers/combobox_dropdown_controller.js';
|
|
@@ -31,3 +35,4 @@ export { default as InputClearableController } from './controllers/input_clearab
|
|
|
31
35
|
export { default as ModalController } from './controllers/modal_controller.js';
|
|
32
36
|
export { default as PannerController } from './controllers/panner_controller.js';
|
|
33
37
|
export { default as PopoverController } from './controllers/popover_controller.js';
|
|
38
|
+
export { default as TimelineController } from './controllers/timeline_controller.js';
|