@dile/utils 3.0.33 → 3.0.35

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.
@@ -19,4 +19,118 @@ describe('dile-calendar', () => {
19
19
  expect(el.shadowRoot.querySelector('.calendar__navigation')).toBeTruthy();
20
20
  expect(el.shadowRoot.querySelectorAll('.calendar__day-button').length).toBeGreaterThan(0);
21
21
  });
22
+
23
+ it('has accessible navigation buttons with aria-label', async () => {
24
+ const el = await renderCalendar('<dile-calendar></dile-calendar>');
25
+ await el.updateComplete;
26
+
27
+ const prevButtons = el.shadowRoot.querySelectorAll('.calendar__previous-button');
28
+ const nextButtons = el.shadowRoot.querySelectorAll('.calendar__next-button');
29
+
30
+ expect(prevButtons.length).toBe(2); // Year and Month prev
31
+ expect(nextButtons.length).toBe(2); // Year and Month next
32
+
33
+ prevButtons.forEach(btn => {
34
+ expect(btn.getAttribute('aria-label')).toBeTruthy();
35
+ });
36
+ nextButtons.forEach(btn => {
37
+ expect(btn.getAttribute('aria-label')).toBeTruthy();
38
+ });
39
+ });
40
+
41
+ it('has accessible grid semantics and screen-reader day labels', async () => {
42
+ const el = await renderCalendar('<dile-calendar></dile-calendar>');
43
+ await el.updateComplete;
44
+
45
+ const gridcells = el.shadowRoot.querySelectorAll('[role="gridcell"]');
46
+ expect(gridcells.length).toBeGreaterThan(0);
47
+
48
+ const dayButtons = el.shadowRoot.querySelectorAll('.calendar__day-button[role="button"]');
49
+ expect(dayButtons.length).toBeGreaterThan(0);
50
+
51
+ const srLabels = el.shadowRoot.querySelectorAll('.calendar__day-button .u-sr-only');
52
+ expect(srLabels.length).toBe(dayButtons.length);
53
+ expect(srLabels[0].textContent.trim().length).toBeGreaterThan(0);
54
+ });
55
+
56
+ it('supports roving tabindex where only one active date has tabindex 0', async () => {
57
+ const el = await renderCalendar('<dile-calendar></dile-calendar>');
58
+ await el.updateComplete;
59
+
60
+ const focusableDays = el.shadowRoot.querySelectorAll('.calendar__day-button[tabindex="0"]');
61
+ expect(focusableDays.length).toBe(1);
62
+
63
+ const unfocusableDays = el.shadowRoot.querySelectorAll('.calendar__day-button[tabindex="-1"]');
64
+ expect(unfocusableDays.length).toBeGreaterThan(0);
65
+ });
66
+
67
+ it('navigates days with keyboard arrow keys', async () => {
68
+ const el = await renderCalendar('<dile-calendar></dile-calendar>');
69
+ await el.updateComplete;
70
+
71
+ const initialDate = new Date(2026, 0, 15);
72
+ el.centralDate = initialDate;
73
+ await el.updateComplete;
74
+
75
+ const contentWrapper = el.shadowRoot.getElementById('js-content-wrapper');
76
+
77
+ // ArrowRight: next day
78
+ contentWrapper.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight', bubbles: true, composed: true }));
79
+ await el.updateComplete;
80
+ expect(el.centralDate.getDate()).toBe(16);
81
+
82
+ // ArrowLeft: previous day
83
+ contentWrapper.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft', bubbles: true, composed: true }));
84
+ await el.updateComplete;
85
+ expect(el.centralDate.getDate()).toBe(15);
86
+
87
+ // ArrowDown: next week (+7 days)
88
+ contentWrapper.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown', bubbles: true, composed: true }));
89
+ await el.updateComplete;
90
+ expect(el.centralDate.getDate()).toBe(22);
91
+
92
+ // ArrowUp: previous week (-7 days)
93
+ contentWrapper.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowUp', bubbles: true, composed: true }));
94
+ await el.updateComplete;
95
+ expect(el.centralDate.getDate()).toBe(15);
96
+ });
97
+
98
+ it('selects date when clicking a day button', async () => {
99
+ const el = await renderCalendar('<dile-calendar></dile-calendar>');
100
+ await el.updateComplete;
101
+
102
+ let selectedDate = null;
103
+ el.addEventListener('user-selected-date-changed', (e) => {
104
+ selectedDate = e.detail.selectedDate;
105
+ });
106
+
107
+ const activeDayButton = el.shadowRoot.querySelector('.calendar__day-button[tabindex="0"]');
108
+ activeDayButton.click();
109
+ await el.updateComplete;
110
+
111
+ expect(selectedDate).toBeTruthy();
112
+ expect(selectedDate instanceof Date).toBe(true);
113
+ });
114
+
115
+ it('navigates next and previous months and years', async () => {
116
+ const el = await renderCalendar('<dile-calendar></dile-calendar>');
117
+ el.centralDate = new Date(2026, 5, 15); // June 2026
118
+ await el.updateComplete;
119
+
120
+ el.goToNextMonth();
121
+ await el.updateComplete;
122
+ expect(el.centralDate.getMonth()).toBe(6); // July
123
+
124
+ el.goToPreviousMonth();
125
+ await el.updateComplete;
126
+ expect(el.centralDate.getMonth()).toBe(5); // June
127
+
128
+ el.goToNextYear();
129
+ await el.updateComplete;
130
+ expect(el.centralDate.getFullYear()).toBe(2027);
131
+
132
+ el.goToPreviousYear();
133
+ await el.updateComplete;
134
+ expect(el.centralDate.getFullYear()).toBe(2026);
135
+ });
22
136
  });
@@ -20,14 +20,26 @@ describe('dile-datepicker', () => {
20
20
  expect(el.shadowRoot.querySelector('dile-iconlib')).toBeTruthy();
21
21
  });
22
22
 
23
- it('updates value when a date is selected from the calendar', async () => {
23
+ it('updates value when a date is selected from the calendar and closes overlay', async () => {
24
24
  const el = await renderDatepicker('<dile-datepicker name="birthdate"></dile-datepicker>');
25
25
  const calendar = el.shadowRoot.querySelector('dile-calendar');
26
+ const menu = el.shadowRoot.getElementById('menu');
27
+
26
28
  calendar.dispatchEvent(new CustomEvent('user-selected-date-changed', {
27
29
  detail: { selectedDate: new Date(2024, 0, 15) },
30
+ bubbles: true,
31
+ composed: true,
28
32
  }));
29
33
  await el.updateComplete;
30
34
 
31
35
  expect(el.value).toBeTruthy();
36
+ expect(menu._overlayClass).toBe('');
37
+ });
38
+
39
+ it('passes firstDayOfWeek to the dile-calendar child', async () => {
40
+ const el = await renderDatepicker('<dile-datepicker name="birthdate" firstDayOfWeek="1"></dile-datepicker>');
41
+ await el.updateComplete;
42
+ const calendar = el.shadowRoot.querySelector('dile-calendar');
43
+ expect(calendar.firstDayOfWeek).toBe(1);
32
44
  });
33
45
  });
@@ -22,11 +22,34 @@ describe('dile-datetimepicker', () => {
22
22
  expect(el.shadowRoot.querySelector('dile-time-picker')).toBeTruthy();
23
23
  });
24
24
 
25
- it('updates value combining date and time when accepted', async () => {
25
+ it('renders an accessible accept button with aria-label and type="button"', async () => {
26
+ const el = await renderDatetimepicker('<dile-datetimepicker name="appointment" acceptLabel="Confirm selection"></dile-datetimepicker>');
27
+ const acceptBtn = el.shadowRoot.querySelector('.accept-button');
28
+
29
+ expect(acceptBtn).toBeTruthy();
30
+ expect(acceptBtn.tagName.toLowerCase()).toBe('button');
31
+ expect(acceptBtn.getAttribute('type')).toBe('button');
32
+ expect(acceptBtn.getAttribute('aria-label')).toBe('Confirm selection');
33
+ expect(acceptBtn.querySelector('dile-icon')).toBeTruthy();
34
+ });
35
+
36
+ it('updates value combining date and time when accepted via method', async () => {
26
37
  const el = await renderDatetimepicker('<dile-datetimepicker name="appointment"></dile-datetimepicker>');
27
38
  el.acceptTime();
28
39
  await el.updateComplete;
29
40
 
30
41
  expect(el.value).toMatch(/^\d{4}-\d{2}-\d{2} /);
31
42
  });
43
+
44
+ it('updates value and closes overlay when accept button is clicked', async () => {
45
+ const el = await renderDatetimepicker('<dile-datetimepicker name="appointment"></dile-datetimepicker>');
46
+ const acceptBtn = el.shadowRoot.querySelector('.accept-button');
47
+ const menu = el.shadowRoot.getElementById('menu');
48
+
49
+ acceptBtn.click();
50
+ await el.updateComplete;
51
+
52
+ expect(el.value).toMatch(/^\d{4}-\d{2}-\d{2} /);
53
+ expect(menu._overlayClass).toBe('');
54
+ });
32
55
  });
@@ -1,5 +1,6 @@
1
1
  import { html, css } from "lit";
2
2
  import { DileInputIcon } from '@dile/ui/components/input/index.js';
3
+ import { DileCloseOnEscPressed } from '@dile/ui/mixins/close-on-esc-pressed';
3
4
  import '@dile/iconlib/lucide-icons/calendar.js';
4
5
  import '@dile/ui/components/menu-overlay/menu-overlay.js';
5
6
 
@@ -7,7 +8,7 @@ import {
7
8
  formatDate,
8
9
  } from '@lion/ui/localize.js';
9
10
 
10
- export class DileDatepicker extends DileInputIcon {
11
+ export class DileDatepicker extends DileCloseOnEscPressed(DileInputIcon) {
11
12
  static get styles() {
12
13
  return [
13
14
  super.styles,
@@ -64,6 +65,7 @@ export class DileDatepicker extends DileInputIcon {
64
65
  verticalAlign: { type: String },
65
66
  moveTop: { type: Number },
66
67
  moveLeft: { type: Number },
68
+ opened: { type: Boolean },
67
69
  };
68
70
  }
69
71
 
@@ -75,6 +77,7 @@ export class DileDatepicker extends DileInputIcon {
75
77
  this.verticalAlign = 'center';
76
78
  this.moveTop = 0;
77
79
  this.moveLeft = 0;
80
+ this.opened = false;
78
81
  }
79
82
 
80
83
  renderIconButton() {
@@ -88,6 +91,8 @@ export class DileDatepicker extends DileInputIcon {
88
91
  verticalAlign="${this.verticalAlign}"
89
92
  horizontalAlign="${this.horizontalAlign}"
90
93
  id="menu"
94
+ @overlay-opened="${this.overlayOpenedHandler}"
95
+ @overlay-closed="${this.overlayClosedHandler}"
91
96
  >
92
97
  <div slot="trigger" class="trigger-container">
93
98
  ${super.renderIconButton()}
@@ -99,11 +104,40 @@ export class DileDatepicker extends DileInputIcon {
99
104
  `;
100
105
  }
101
106
 
107
+ overlayOpenedHandler() {
108
+ this.opened = true;
109
+ }
110
+
111
+ overlayClosedHandler() {
112
+ this.opened = false;
113
+ }
114
+
102
115
  showDate(e) {
103
116
  let date = formatDate(e.detail.selectedDate);
104
117
  this.value = date;
105
- this.shadowRoot.getElementById('menu').close();
106
- }
118
+ const menu = this.shadowRoot.getElementById('menu');
119
+ if (menu) {
120
+ menu.close();
121
+ }
122
+ }
123
+
124
+ close() {
125
+ const menu = this.shadowRoot.getElementById('menu');
126
+ if (menu) {
127
+ menu.close();
128
+ }
129
+ }
130
+
131
+ openDatepicker() {
132
+ const menu = this.shadowRoot.getElementById('menu');
133
+ if (menu) {
134
+ menu.open();
135
+ }
136
+ }
137
+
138
+ closeDatepicker() {
139
+ this.close();
140
+ }
107
141
 
108
142
  get contentTemplate() {
109
143
  return html`
@@ -51,6 +51,22 @@ export class DileDatetimepicker extends DileDatepicker {
51
51
  display: flex;
52
52
  align-items: center;
53
53
  }
54
+ .accept-button {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ background: transparent;
59
+ border: none;
60
+ padding: 0;
61
+ margin: 0;
62
+ cursor: pointer;
63
+ border-radius: 50%;
64
+ outline: none;
65
+ }
66
+ .accept-button:focus-visible {
67
+ outline: 2px solid var(--dile-datetimepicker-accept-focus-ring-color, var(--dile-primary-color, #2563eb));
68
+ outline-offset: 2px;
69
+ }
54
70
  .time-area dile-icon {
55
71
  --dile-icon-size: var(--dile-datetimepicker-accept-icon-size, 24px);
56
72
  --dile-icon-color: var(--dile-datetimepicker-accept-icon-color, var(--dile-on-primary-color, #888));
@@ -63,9 +79,17 @@ export class DileDatetimepicker extends DileDatepicker {
63
79
  `];
64
80
  }
65
81
 
82
+ static get properties() {
83
+ return {
84
+ ...super.properties,
85
+ acceptLabel: { type: String },
86
+ };
87
+ }
88
+
66
89
  constructor() {
67
90
  super();
68
91
  this.icon = 'lucide.calendar-clock';
92
+ this.acceptLabel = 'Accept date and time';
69
93
  }
70
94
 
71
95
  get contentTemplate() {
@@ -80,7 +104,15 @@ export class DileDatetimepicker extends DileDatepicker {
80
104
  <span>
81
105
  <dile-time-picker></dile-time-picker>
82
106
  </span>
83
- <dile-icon rounded .icon=${doneIcon} @click=${this.acceptTime}></dile-icon>
107
+ <button
108
+ type="button"
109
+ class="accept-button"
110
+ aria-label="${this.acceptLabel}"
111
+ title="${this.acceptLabel}"
112
+ @click=${this.acceptTime}
113
+ >
114
+ <dile-icon rounded .icon=${doneIcon}></dile-icon>
115
+ </button>
84
116
  </div>
85
117
  </div>
86
118
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dile/utils",
3
- "version": "3.0.33",
3
+ "version": "3.0.35",
4
4
  "description": "Utility Components of Diverse Uses Based on the Lit Library and Web Components Standard.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,8 +21,8 @@
21
21
  "homepage": "https://dile-components.com/",
22
22
  "dependencies": {
23
23
  "@dile/iconlib": "^1.8.1",
24
- "@dile/ui": "^3.6.6",
25
- "@lion/ui": "^0.11.2",
24
+ "@dile/ui": "^3.6.7",
25
+ "@lion/ui": "^0.21.0",
26
26
  "linkify-string": "^4.1.3",
27
27
  "linkifyjs": "^4.1.3",
28
28
  "lit": "^2.7.0 || ^3.0.0"
@@ -30,5 +30,5 @@
30
30
  "publishConfig": {
31
31
  "access": "public"
32
32
  },
33
- "gitHead": "3bc0a2459a5ced3e7105ce061f93f07a9c798759"
33
+ "gitHead": "d62c55983a8fcdf7b1a4cb2b6da64d4d1e30b9c6"
34
34
  }