@ditojs/ui 2.100.0 → 3.0.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/ui",
3
- "version": "2.100.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js UI is a simple UI library, built with Vue.js for use in Dito.js Admin and elsewhere",
6
6
  "repository": "https://github.com/ditojs/dito/tree/main/packages/ui",
@@ -38,12 +38,11 @@
38
38
  "ie_mob >= 11"
39
39
  ],
40
40
  "dependencies": {
41
- "@ditojs/utils": "^2.100.0",
42
- "@soda/get-current-script": "^1.0.2"
41
+ "@ditojs/utils": "^3.0.0"
43
42
  },
44
43
  "devDependencies": {
45
- "@ditojs/build": "^2.100.0",
44
+ "@ditojs/build": "^3.0.0",
46
45
  "vite": "^8.3.2"
47
46
  },
48
- "gitHead": "c6fdbe03971188779fc9cf5f1b4202e08bc75a65"
47
+ "gitHead": "399f683a6ca227a03cfc0978fee9055383e6b3d8"
49
48
  }
@@ -1,6 +1,9 @@
1
1
  <!-- Derived from ATUI, and further extended: https://aliqin.github.io/atui/ -->
2
2
  <template lang="pug">
3
- .dito-calendar
3
+ .dito-calendar(
4
+ role="dialog"
5
+ aria-label="Choose date"
6
+ )
4
7
  .dito-calendar-popup
5
8
  .dito-calendar-inner
6
9
  template(
@@ -8,22 +11,40 @@
8
11
  )
9
12
  .dito-calendar-header
10
13
  a.dito-calendar-step-prev.dito-calendar-step-year(
14
+ role="button"
15
+ tabindex="0"
16
+ aria-label="Previous year"
11
17
  @click="stepYear(-1)"
12
18
  )
13
19
  a.dito-calendar-step-prev.dito-calendar-step-month(
20
+ role="button"
21
+ tabindex="0"
22
+ aria-label="Previous month"
14
23
  @click="stepMonth(-1)"
15
24
  )
16
25
  span
17
26
  a.dito-calendar-select-year(
27
+ role="button"
28
+ tabindex="0"
29
+ aria-label="Switch to year selection"
18
30
  @click="setMode('year')"
19
31
  ) {{ dateToString(currentValue, { year: true }) }}
20
32
  a.dito-calendar-select-month(
33
+ role="button"
34
+ tabindex="0"
35
+ aria-label="Switch to month selection"
21
36
  @click="setMode('month')"
22
37
  ) {{ dateToString(currentValue, { month: true }) }}
23
38
  a.dito-calendar-step-next.dito-calendar-step-month(
39
+ role="button"
40
+ tabindex="0"
41
+ aria-label="Next month"
24
42
  @click="stepMonth(1)"
25
43
  )
26
44
  a.dito-calendar-step-next.dito-calendar-step-year(
45
+ role="button"
46
+ tabindex="0"
47
+ aria-label="Next year"
27
48
  @click="stepYear(1)"
28
49
  )
29
50
  .dito-calendar-body
@@ -31,15 +52,21 @@
31
52
  span(
32
53
  v-for="weekday in weekdayNames"
33
54
  ) {{ weekday.short }}
34
- .dito-calendar-dates
55
+ .dito-calendar-dates(role="grid")
35
56
  span(
36
57
  v-for="date in dateRange"
37
58
  :class="date.state && `dito-calendar-item-${date.state}`"
59
+ role="gridcell"
60
+ :aria-label="dateToString(date.date)"
61
+ :aria-selected="date.state === 'active'"
62
+ :aria-disabled="date.state === 'disabled'"
38
63
  @click="date.state !== 'disabled' && selectDate(date.date)"
39
64
  ) {{ date.text }}
40
65
  .dito-calendar-footer
41
66
  a.dito-calendar-select-now(
42
67
  role="button"
68
+ tabindex="0"
69
+ :aria-label="`Select today (${dateToString(new Date())})`"
43
70
  :title="dateToString(new Date())"
44
71
  @click="selectDate(new Date())"
45
72
  )
@@ -48,20 +75,35 @@
48
75
  )
49
76
  .dito-calendar-header
50
77
  a.dito-calendar-step-prev(
78
+ role="button"
79
+ tabindex="0"
80
+ aria-label="Previous year"
51
81
  @click="stepYear(-1)"
52
82
  )
53
83
  span
54
84
  a.dito-calendar-select-year(
85
+ role="button"
86
+ tabindex="0"
87
+ aria-label="Switch to year selection"
55
88
  @click="setMode('year')"
56
89
  ) {{ dateToString(currentValue, { year: true }) }}
57
90
  a.dito-calendar-step-next(
91
+ role="button"
92
+ tabindex="0"
93
+ aria-label="Next year"
58
94
  @click="stepYear(1)"
59
95
  )
60
96
  .dito-calendar-body
61
- .dito-calendar-months
97
+ .dito-calendar-months(
98
+ role="listbox"
99
+ aria-label="Month"
100
+ )
62
101
  span(
63
102
  v-for="(month, index) in monthNames"
64
103
  :class="getMonthClass(index)"
104
+ role="option"
105
+ :aria-label="month.long"
106
+ :aria-selected="getMonthClass(index)['dito-calendar-item-active']"
65
107
  @click="selectMonth(index)"
66
108
  ) {{ month.short }}
67
109
  template(
@@ -69,17 +111,29 @@
69
111
  )
70
112
  .dito-calendar-header
71
113
  a.dito-calendar-step-prev(
114
+ role="button"
115
+ tabindex="0"
116
+ aria-label="Previous decade"
72
117
  @click="stepDecade(-1)"
73
118
  )
74
119
  span {{ decadeToString(currentValue) }}
75
120
  a.dito-calendar-step-next(
121
+ role="button"
122
+ tabindex="0"
123
+ aria-label="Next decade"
76
124
  @click="stepDecade(1)"
77
125
  )
78
126
  .dito-calendar-body
79
- .dito-calendar-years
127
+ .dito-calendar-years(
128
+ role="listbox"
129
+ aria-label="Year"
130
+ )
80
131
  span(
81
132
  v-for="year in yearRange"
82
133
  :class="getYearClass(year)"
134
+ role="option"
135
+ :aria-label="String(year)"
136
+ :aria-selected="getYearClass(year)['dito-calendar-item-active']"
83
137
  @click="selectYear(year)"
84
138
  ) {{ year }}
85
139
  </template>
@@ -32,34 +32,58 @@ DitoTrigger.dito-time-picker(
32
32
  )
33
33
  slot(name="suffix")
34
34
  template(#popup)
35
- .dito-time-picker-popup
35
+ .dito-time-picker-popup(
36
+ role="dialog"
37
+ aria-label="Choose time"
38
+ )
36
39
  .dito-time-picker-panel
37
- ul.dito-time-picker-hour(ref="hour")
40
+ ul.dito-time-picker-hour(
41
+ ref="hour"
42
+ role="listbox"
43
+ aria-label="Hour"
44
+ )
38
45
  template(
39
46
  v-for="index in 24"
40
47
  )
41
48
  li(
42
49
  v-if="!disabledHour(index - 1)"
50
+ role="option"
51
+ :aria-label="leftPad(index - 1)"
52
+ :aria-selected="hour === index - 1"
43
53
  :class="{ selected: hour === index - 1 }"
44
54
  @click="hour = index - 1"
45
55
  ) {{ leftPad(index - 1) }}
46
56
  .dito-time-picker-panel
47
- ul.dito-time-picker-minute(ref="minute")
57
+ ul.dito-time-picker-minute(
58
+ ref="minute"
59
+ role="listbox"
60
+ aria-label="Minute"
61
+ )
48
62
  template(
49
63
  v-for="index in 60"
50
64
  )
51
65
  li(
52
66
  v-if="!disabledMinute(index - 1)"
67
+ role="option"
68
+ :aria-label="leftPad(index - 1)"
69
+ :aria-selected="minute === index - 1"
53
70
  :class="{ selected: minute === index - 1 }"
54
71
  @click="minute = index - 1"
55
72
  ) {{ leftPad(index - 1) }}
56
73
  .dito-time-picker-panel
57
- ul.dito-time-picker-second(ref="second")
74
+ ul.dito-time-picker-second(
75
+ ref="second"
76
+ role="listbox"
77
+ aria-label="Second"
78
+ )
58
79
  template(
59
80
  v-for="index in 60"
60
81
  )
61
82
  li(
62
83
  v-if="!disabledSecond(index - 1)"
84
+ role="option"
85
+ :aria-label="leftPad(index - 1)"
86
+ :aria-selected="second === index - 1"
63
87
  :class="{ selected: second === index - 1 }"
64
88
  @click="second = index - 1"
65
89
  ) {{ leftPad(index - 1) }}
@@ -174,6 +174,10 @@ export default {
174
174
  },
175
175
 
176
176
  methods: {
177
+ isPopupFocused() {
178
+ return !!this.$refs.popup?.matches(':focus-within')
179
+ },
180
+
177
181
  updatePosition() {
178
182
  const { trigger, popup } = this.$refs
179
183
  if (this.show && popup.offsetWidth === 0) {
@@ -291,7 +295,7 @@ export default {
291
295
  blur: () => {
292
296
  // Use timeout to allow clicked inputs to grab focus
293
297
  this.blurTimer = setTimeout(() => {
294
- if (!this.$refs.popup.matches(':focus-within')) {
298
+ if (!this.isPopupFocused()) {
295
299
  this.showPopup = false
296
300
  }
297
301
  }, 0)
@@ -323,7 +327,12 @@ export default {
323
327
  // becomes editable and still focused again.
324
328
  setTimeout(() => {
325
329
  input.removeAttribute('readonly')
326
- if (!event.target.matches('input, textarea, button')) {
330
+ // Only bring the focus back from the popup, not if the focus
331
+ // moved elsewhere in the meantime, e.g. by clicking outside.
332
+ if (
333
+ !event.target.matches('input, textarea, button') &&
334
+ this.isPopupFocused()
335
+ ) {
327
336
  input.focus()
328
337
  }
329
338
  input = null
package/src/utils/date.js CHANGED
@@ -20,6 +20,11 @@ export function parseDate(string, {
20
20
  time = true
21
21
  } = {}) {
22
22
  const timeDefault = time ? null : 0
23
+ // Extract the 12-hour clock period, e.g. in en-US "11:30:00 AM".
24
+ const period = string?.match(/\s*\b([ap])\.?m\.?$/i)
25
+ if (period) {
26
+ string = string.slice(0, period.index)
27
+ }
23
28
  let [
24
29
  day,
25
30
  sep1,
@@ -36,10 +41,19 @@ export function parseDate(string, {
36
41
  millisecond = timeDefault
37
42
  ] = getDateParts(string, { date, time })
38
43
  if (year && year.length >= 4) {
39
- if (locale === 'en-US' && sep1 === '/' && sep2 === '/') {
44
+ if (
40
45
  // American format: MM/DD/YYYY
46
+ (locale === 'en-US' && sep1 === '/' && sep2 === '/') ||
47
+ // Month name first, e.g. en-US "May 14, 2026"
48
+ (isNaN(+day) && !isNaN(+month))
49
+ ) {
41
50
  ;[day, month] = [month, day]
42
51
  }
52
+ if (period && hour !== null) {
53
+ const pm = period[1].toLowerCase() === 'p'
54
+ hour = (+hour % 12) + (pm ? 12 : 0)
55
+ hour = String(hour).padStart(2, '0')
56
+ }
43
57
  month = getMonthIndex(month, { locale })
44
58
  const isValidTimePart = part => part === null || part.length === 2
45
59
  if (
@@ -51,7 +65,7 @@ export function parseDate(string, {
51
65
  )
52
66
  )
53
67
  ) {
54
- return new Date(
68
+ const result = new Date(
55
69
  +year,
56
70
  +month,
57
71
  +day,
@@ -60,6 +74,11 @@ export function parseDate(string, {
60
74
  +second,
61
75
  +millisecond
62
76
  )
77
+ // Reject invalid dates, including out-of-range parts that `Date` would
78
+ // roll over, e.g. day 45.
79
+ return result.getMonth() === +month && result.getDate() === +day
80
+ ? result
81
+ : null
63
82
  }
64
83
  }
65
84
  return null
@@ -0,0 +1,38 @@
1
+ import { parseDate } from './date.js'
2
+
3
+ describe('parseDate()', () => {
4
+ const date = { day: 'numeric', month: 'long', year: 'numeric' }
5
+ const time = { hour: '2-digit', minute: '2-digit', second: '2-digit' }
6
+
7
+ it.each([
8
+ ['May 14, 2026', new Date(2026, 4, 14)],
9
+ ['14 May 2026', new Date(2026, 4, 14)],
10
+ ['05/14/2026', new Date(2026, 4, 14)],
11
+ ['14.05.2026', new Date(2026, 4, 14)]
12
+ ])('parses date %o', (string, expected) => {
13
+ expect(parseDate(string, { date, time: null })).toEqual(expected)
14
+ })
15
+
16
+ it.each([
17
+ ['May 14, 2026, 11:30:00 AM', new Date(2026, 4, 14, 11, 30, 0)],
18
+ ['May 14, 2026, 11:30:00 PM', new Date(2026, 4, 14, 23, 30, 0)],
19
+ ['May 14, 2026, 12:05:00 AM', new Date(2026, 4, 14, 0, 5, 0)],
20
+ ['May 14, 2026, 12:05:00 PM', new Date(2026, 4, 14, 12, 5, 0)],
21
+ ['14.05.2026, 23:30:00', new Date(2026, 4, 14, 23, 30, 0)]
22
+ ])('parses datetime %o', (string, expected) => {
23
+ expect(parseDate(string, { date, time })).toEqual(expected)
24
+ })
25
+
26
+ it('parses time %o', () => {
27
+ expect(parseDate('11:30:00 PM', { date: null, time })).toEqual(
28
+ new Date(2000, 0, 1, 23, 30, 0)
29
+ )
30
+ })
31
+
32
+ it.each(['', 'May', 'Foo 14, 2026', '2026-05-14', '13/45/2026'])(
33
+ 'returns null for %o',
34
+ string => {
35
+ expect(parseDate(string, { date, time: null })).toBe(null)
36
+ }
37
+ )
38
+ })