diff --git a/aform/api.md b/aform/api.md
index bce9b6c1..9ff963b1 100644
--- a/aform/api.md
+++ b/aform/api.md
@@ -36,6 +36,22 @@ Vue component exported from @stonecrop/aform.
import { ADatePicker } from '@stonecrop/aform'
```
+### ADateSelection
+
+Vue component exported from @stonecrop/aform.
+
+```typescript
+import { ADateSelection } from '@stonecrop/aform'
+```
+
+### ADateTime
+
+Vue component exported from @stonecrop/aform.
+
+```typescript
+import { ADateTime } from '@stonecrop/aform'
+```
+
### ADropdown
Vue component exported from @stonecrop/aform.
@@ -136,6 +152,7 @@ Defined props for AForm components
export type ComponentProps = {
schema?: SchemaTypes;
label?: string;
+ selectRange?: boolean;
mask?: string;
required?: boolean;
mode?: FormMode;
diff --git a/aform/src/components/form/ADate.vue b/aform/src/components/form/ADate.vue
index 896aa190..bda6fc8f 100644
--- a/aform/src/components/form/ADate.vue
+++ b/aform/src/components/form/ADate.vue
@@ -1,7 +1,7 @@
- {{ inputDate ? new Date(inputDate).toLocaleDateString() : '' }}
+ {{ modelValue ? new Date(inputDate).toLocaleDateString() : '' }}
@@ -9,42 +9,73 @@
:id="uuid"
ref="date"
v-model="inputDate"
+ class="adate-input"
+ :value="inputDate"
type="date"
:disabled="mode === 'read'"
:required="required"
- @click="showPicker" />
+ @click.prevent="
+ () => {
+ showPicker = !showPicker
+ }
+ " />
+
diff --git a/aform/src/components/form/ADatePicker.vue b/aform/src/components/form/ADatePicker.vue
index 70131ae6..d4eca97c 100644
--- a/aform/src/components/form/ADatePicker.vue
+++ b/aform/src/components/form/ADatePicker.vue
@@ -11,6 +11,30 @@
{{ monthAndYear }} |
> |
+
+ |
+
+
+ |
+
@@ -44,7 +73,8 @@
@@ -175,6 +360,7 @@ defineExpose({ currentMonth, currentYear, selectedDate })
color: var(--sc-cell-text-color);
outline: none;
border-collapse: collapse;
+ margin-bottom: 10px;
/* width: calc(100% - 4px); */
}
@@ -190,26 +376,45 @@ defineExpose({ currentMonth, currentYear, selectedDate })
outline: 2px solid transparent;
min-width: 3ch;
max-width: 3ch;
+ cursor: pointer;
+}
+.adatepicker td.date-cell:hover {
+ background: var(--sc-gray-10);
}
.adatepicker td:focus,
.adatepicker td:focus-within {
- outline: 1px dashed black;
+ /* outline: 1px dashed black; */
box-shadow: none;
overflow: hidden;
min-height: 1.15em;
max-height: 1.15em;
overflow: hidden;
}
-.adatepicker .selectedDate {
- outline: 1px solid black;
+.adatepicker .selectedDate,
+.adatepicker .startDate,
+.adatepicker .endDate {
+ /* outline: 1px solid black; */
background: var(--sc-gray-20);
font-weight: bolder;
}
+.adatepicker .startDate {
+ /* border-radius: 5px 0px 0px 5px; */
+ border-left: 1px solid var(--sc-gray-50);
+ background: var(--sc-gray-20) !important;
+}
+.adatepicker .endDate {
+ border-right: 1px solid var(--sc-gray-50);
+ /* border-radius: 0px 5px 5px 0px; */
+ background: var(--sc-gray-20) !important;
+}
+.adatepicker .withinRange {
+ background: var(--sc-gray-5);
+}
.adatepicker .todaysDate {
font-weight: bolder;
- text-decoration: underline;
+ /* text-decoration: underline; */
color: black;
}
.days-header > td {
@@ -218,4 +423,15 @@ defineExpose({ currentMonth, currentYear, selectedDate })
.prev-date {
color: var(--sc-gray-20);
}
+
+.adatepicker .date-input {
+ display: flex;
+ width: 100%;
+ gap: 5px;
+ align-items: center;
+}
+.adatepicker .date-input > input {
+ width: 50%;
+ padding: 2px;
+}
diff --git a/aform/src/components/form/ADateSelection.vue b/aform/src/components/form/ADateSelection.vue
new file mode 100644
index 00000000..03091ba6
--- /dev/null
+++ b/aform/src/components/form/ADateSelection.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
diff --git a/aform/src/components/form/ADateTime.vue b/aform/src/components/form/ADateTime.vue
new file mode 100644
index 00000000..a0a7b6ed
--- /dev/null
+++ b/aform/src/components/form/ADateTime.vue
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
diff --git a/aform/src/index.ts b/aform/src/index.ts
index 391d5d16..09ef82a0 100644
--- a/aform/src/index.ts
+++ b/aform/src/index.ts
@@ -7,6 +7,8 @@ import AComboBox from './components/form/AComboBox.vue'
import ADate from './components/form/ADate.vue'
import ADropdown from './components/form/ADropdown.vue'
import ADatePicker from './components/form/ADatePicker.vue'
+import ADateTime from './components/form/ADateTime.vue'
+import ADateSelection from './components/form/ADateSelection.vue'
import AFieldset from './components/form/AFieldset.vue'
import AFileAttach from './components/form/AFileAttach.vue'
import AForm from './components/AForm.vue'
@@ -28,6 +30,8 @@ function install(app: App /* options */) {
app.component('ADate', ADate)
app.component('ADropdown', ADropdown)
app.component('ADatePicker', ADatePicker)
+ app.component('ADateTime', ADateTime)
+ app.component('ADateSelection', ADateSelection)
app.component('AFieldset', AFieldset)
app.component('AFileAttach', AFileAttach)
app.component('AForm', AForm)
@@ -41,6 +45,8 @@ export {
ADate,
ADropdown,
ADatePicker,
+ ADateSelection,
+ ADateTime,
AFieldset,
AFileAttach,
AForm,
diff --git a/aform/src/types/index.ts b/aform/src/types/index.ts
index ee984937..8aa37f29 100644
--- a/aform/src/types/index.ts
+++ b/aform/src/types/index.ts
@@ -23,6 +23,9 @@ export type ComponentProps = {
*/
label?: string
+ // TODO: add docstring
+ selectRange?: boolean
+
/**
* The mask to apply to inputs inside the component. Accepts either a plain
* mask string (e.g. `"(###) ###-####"`) or a stringified arrow function that
diff --git a/aform/tests/date-selection.spec.ts b/aform/tests/date-selection.spec.ts
new file mode 100644
index 00000000..b3b2f02e
--- /dev/null
+++ b/aform/tests/date-selection.spec.ts
@@ -0,0 +1,101 @@
+import { describe, it, expect } from 'vitest'
+import { mount } from '@vue/test-utils'
+
+import ADateSelection from '../src/components/form/ADateSelection.vue'
+import ADatePicker from '../src/components/form/ADatePicker.vue'
+import ADateTime from '../src/components/form/ADateTime.vue'
+
+describe('date-selection component', () => {
+ const globalComponents = {
+ global: {
+ components: {
+ ADatePicker,
+ ADateTime,
+ },
+ },
+ }
+
+ it('renders date picker and time picker by default', () => {
+ const wrapper = mount(ADateSelection, globalComponents)
+ expect(wrapper.find('.adatepicker').exists()).toBe(true)
+ expect(wrapper.find('.adate_time').exists()).toBe(true)
+ })
+
+ it('renders only date picker when showTime is false', () => {
+ const wrapper = mount(ADateSelection, {
+ ...globalComponents,
+ props: { showTime: false },
+ })
+ expect(wrapper.find('.adatepicker').exists()).toBe(true)
+ expect(wrapper.find('.adate_time').exists()).toBe(false)
+ })
+
+ it('renders only time picker when showDate is false', () => {
+ const wrapper = mount(ADateSelection, {
+ ...globalComponents,
+ props: { showDate: false },
+ })
+ expect(wrapper.find('.adatepicker').exists()).toBe(false)
+ expect(wrapper.find('.adate_time').exists()).toBe(true)
+ })
+
+ it('renders empty message when neither date nor time is shown', () => {
+ const wrapper = mount(ADateSelection, {
+ ...globalComponents,
+ props: { showDate: false, showTime: false },
+ })
+ expect(wrapper.find('p.empty').exists()).toBe(true)
+ expect(wrapper.find('p.empty').text()).toBe('empty')
+ })
+
+ it('emits get-date when date is selected', async () => {
+ const wrapper = mount(ADateSelection, globalComponents)
+ const testDate = new Date(2023, 5, 15)
+ const datePicker = wrapper.findComponent(ADatePicker)
+ await datePicker.vm.$emit('get-date', { selected: testDate, start: null, end: null })
+ const emitted = wrapper.emitted('get-date')
+ expect(emitted).toBeTruthy()
+ expect(emitted![0][0]).toEqual({ selected: testDate, start: null, end: null })
+ })
+
+ it('emits get-time when time is selected', async () => {
+ const wrapper = mount(ADateSelection, globalComponents)
+ const timeData = { hours: 3, minutes: 30, seconds: 0, meridiem: 'PM', militaryTime: 15 }
+ const dateTime = wrapper.findComponent(ADateTime)
+ await dateTime.vm.$emit('get-time', timeData)
+ const emitted = wrapper.emitted('get-time')
+ expect(emitted).toBeTruthy()
+ // ADateTime emits on mount, so our event is the last one
+ expect(emitted![emitted!.length - 1][0]).toEqual(timeData)
+ })
+
+ it('passes selectRange prop to date picker', () => {
+ const wrapper = mount(ADateSelection, {
+ ...globalComponents,
+ props: { selectRange: false },
+ })
+ const datePicker = wrapper.findComponent(ADatePicker)
+ expect(datePicker.props('selectRange')).toBe(false)
+ })
+
+ it('passes time props to time picker', () => {
+ const wrapper = mount(ADateSelection, {
+ ...globalComponents,
+ props: {
+ allowMilitaryTime: true,
+ defaultHours: 10,
+ defaultMinutes: 30,
+ defaultSeconds: 45,
+ defaultMeridiem: 'PM',
+ useSeconds: false,
+ },
+ })
+ const dateTime = wrapper.findComponent(ADateTime)
+ expect(dateTime.props('allowMilitaryTime')).toBe(true)
+ expect(dateTime.props('defaultHours')).toBe(10)
+ expect(dateTime.props('defaultMinutes')).toBe(30)
+ expect(dateTime.props('defaultSeconds')).toBe(45)
+ expect(dateTime.props('defaultMeridiem')).toBe('PM')
+ expect(dateTime.props('useSeconds')).toBe(false)
+ })
+})
diff --git a/aform/tests/date.spec.ts b/aform/tests/date.spec.ts
index 9509605b..72092fb0 100644
--- a/aform/tests/date.spec.ts
+++ b/aform/tests/date.spec.ts
@@ -2,10 +2,23 @@ import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ADate from '../src/components/form/ADate.vue'
+import ADateSelection from '../src/components/form/ADateSelection.vue'
+import ADatePicker from '../src/components/form/ADatePicker.vue'
+import ADateTime from '../src/components/form/ADateTime.vue'
+
+const globalComponents = {
+ global: {
+ components: {
+ ADateSelection,
+ ADatePicker,
+ ADateTime,
+ },
+ },
+}
describe('date component', () => {
it('date input is rendered', async () => {
- const wrapper = mount(ADate)
+ const wrapper = mount(ADate, globalComponents)
const $input = wrapper.find('input')
expect($input.exists()).toBe(true)
expect($input.attributes('type')).toBe('date')
@@ -13,6 +26,7 @@ describe('date component', () => {
it('date input is rendered with value', async () => {
const wrapper = mount(ADate, {
+ ...globalComponents,
props: {
modelValue: '2021-01-01',
},
@@ -24,6 +38,7 @@ describe('date component', () => {
it('date input is disabled by default', async () => {
const wrapper = mount(ADate, {
+ ...globalComponents,
props: {
mode: 'read',
},
@@ -34,7 +49,7 @@ describe('date component', () => {
})
it('date input is required', async () => {
- const wrapper = mount(ADate)
+ const wrapper = mount(ADate, globalComponents)
const $input = wrapper.find('input')
// TODO: setup environment to test spawning the datepicker
@@ -43,7 +58,7 @@ describe('date component', () => {
})
it('formats date value on input change', async () => {
- const wrapper = mount(ADate)
+ const wrapper = mount(ADate, globalComponents)
const $input = wrapper.find('input')
await $input.setValue('2023-06-15')
await wrapper.vm.$nextTick()
@@ -52,6 +67,7 @@ describe('date component', () => {
it('renders in display mode with formatted date', () => {
const wrapper = mount(ADate, {
+ ...globalComponents,
props: { modelValue: '2021-01-01', mode: 'display' },
})
expect(wrapper.find('input').exists()).toBe(false)
@@ -59,7 +75,10 @@ describe('date component', () => {
})
it('renders in display mode with empty span when no value', () => {
- const wrapper = mount(ADate, { props: { mode: 'display' } })
+ const wrapper = mount(ADate, {
+ ...globalComponents,
+ props: { mode: 'display' },
+ })
expect(wrapper.find('input').exists()).toBe(false)
expect(wrapper.find('.aform_display-value').text()).toBe('')
})
diff --git a/aform/tests/datetime.spec.ts b/aform/tests/datetime.spec.ts
new file mode 100644
index 00000000..5273f472
--- /dev/null
+++ b/aform/tests/datetime.spec.ts
@@ -0,0 +1,314 @@
+import { describe, it, expect, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+
+import ADateTime from '../src/components/form/ADateTime.vue'
+
+describe('datetime component', () => {
+ it('renders time inputs with default values', () => {
+ const wrapper = mount(ADateTime)
+ const inputs = wrapper.findAll('input[type="text"]')
+ expect(inputs.length).toBe(3) // hours, minutes, seconds
+ expect(inputs[0].element.value).toBe('12')
+ expect(inputs[1].element.value).toBe('00')
+ expect(inputs[2].element.value).toBe('00')
+ })
+
+ it('emits get-time on mount', () => {
+ const wrapper = mount(ADateTime)
+ const emitted = wrapper.emitted('get-time')
+ expect(emitted).toBeTruthy()
+ expect(emitted![0][0]).toMatchObject({
+ hours: 12,
+ minutes: 0,
+ seconds: 0,
+ meridiem: 'AM',
+ })
+ })
+
+ it('renders meridiem selector by default', () => {
+ const wrapper = mount(ADateTime)
+ const select = wrapper.find('select')
+ expect(select.exists()).toBe(true)
+ expect(select.element.value).toBe('AM')
+ })
+
+ it('does not render meridiem in military time mode', () => {
+ const wrapper = mount(ADateTime, {
+ props: { allowMilitaryTime: true },
+ })
+ expect(wrapper.find('select').exists()).toBe(false)
+ })
+
+ it('does not render seconds when useSeconds is false', () => {
+ const wrapper = mount(ADateTime, {
+ props: { useSeconds: false },
+ })
+ const inputs = wrapper.findAll('input[type="text"]')
+ expect(inputs.length).toBe(2)
+ })
+
+ it('updates hours and emits on blur', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(3)
+ await hoursInput.trigger('blur')
+ const emitted = wrapper.emitted('get-time')
+ const lastEmit = emitted![emitted!.length - 1][0] as any
+ expect(lastEmit.hours).toBe(3)
+ })
+
+ it('updates minutes and emits on enter key', async () => {
+ const wrapper = mount(ADateTime)
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ await minutesInput.setValue(45)
+ await minutesInput.trigger('keydown.enter')
+ const emitted = wrapper.emitted('get-time')
+ const lastEmit = emitted![emitted!.length - 1][0] as any
+ expect(lastEmit.minutes).toBe(45)
+ })
+
+ it('changes meridiem and emits', async () => {
+ const wrapper = mount(ADateTime)
+ const select = wrapper.find('select')
+ await select.setValue('PM')
+ await select.trigger('change')
+ const emitted = wrapper.emitted('get-time')
+ const lastEmit = emitted![emitted!.length - 1][0] as any
+ expect(lastEmit.meridiem).toBe('PM')
+ })
+
+ it('increments hours with up arrow', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.trigger('keydown.up')
+ expect(hoursInput.element.value).toBe('01')
+ })
+
+ it('decrements hours with down arrow', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.trigger('keydown.down')
+ expect(hoursInput.element.value).toBe('11')
+ })
+
+ it('increments minutes with up arrow', async () => {
+ const wrapper = mount(ADateTime)
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ await minutesInput.trigger('keydown.up')
+ expect(minutesInput.element.value).toBe('01')
+ })
+
+ it('decrements seconds with down arrow', async () => {
+ const wrapper = mount(ADateTime)
+ const secondsInput = wrapper.findAll('input[type="text"]')[2]
+ await secondsInput.trigger('keydown.down')
+ expect(secondsInput.element.value).toBe('59')
+ })
+
+ it('wraps hours from 12 to 1 in non-military mode', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(12)
+ await hoursInput.trigger('keydown.up')
+ expect(hoursInput.element.value).toBe('01')
+ })
+
+ it('wraps hours from 1 to 12 in non-military mode', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(1)
+ await hoursInput.trigger('keydown.down')
+ expect(hoursInput.element.value).toBe('12')
+ })
+
+ it('allows 0-23 hours in military mode', async () => {
+ const wrapper = mount(ADateTime, {
+ props: { allowMilitaryTime: true },
+ })
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(23)
+ await hoursInput.trigger('blur')
+ expect(hoursInput.element.value).toBe('23')
+ })
+
+ it('wraps hours from 23 to 0 in military mode', async () => {
+ const wrapper = mount(ADateTime, {
+ props: { allowMilitaryTime: true },
+ })
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(23)
+ await hoursInput.trigger('keydown.up')
+ expect(hoursInput.element.value).toBe('00')
+ })
+
+ it('wraps hours from 0 to 23 in military mode', async () => {
+ const wrapper = mount(ADateTime, {
+ props: { allowMilitaryTime: true, defaultHours: 0 },
+ })
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.trigger('keydown.down')
+ expect(hoursInput.element.value).toBe('23')
+ })
+
+ it('clamps hours to max on blur', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(99)
+ await hoursInput.trigger('blur')
+ expect(hoursInput.element.value).toBe('12')
+ })
+
+ it('clamps minutes to 59 on blur', async () => {
+ const wrapper = mount(ADateTime)
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ await minutesInput.setValue(99)
+ await minutesInput.trigger('blur')
+ expect(minutesInput.element.value).toBe('59')
+ })
+
+ it('clamps seconds to 59 on blur', async () => {
+ const wrapper = mount(ADateTime)
+ const secondsInput = wrapper.findAll('input[type="text"]')[2]
+ await secondsInput.setValue(99)
+ await secondsInput.trigger('blur')
+ expect(secondsInput.element.value).toBe('59')
+ })
+
+ it('changes meridiem when crossing 11-12 boundary upward', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(11)
+ await hoursInput.trigger('keydown.up')
+ const select = wrapper.find('select')
+ expect(select.element.value).toBe('PM')
+ })
+
+ it('changes meridiem when crossing 12-11 boundary downward', async () => {
+ const wrapper = mount(ADateTime, {
+ props: { defaultMeridiem: 'PM' },
+ })
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(12)
+ await hoursInput.trigger('keydown.down')
+ const select = wrapper.find('select')
+ expect(select.element.value).toBe('AM')
+ })
+
+ it('emits correct militaryTime for PM hours', async () => {
+ const wrapper = mount(ADateTime, {
+ props: { defaultMeridiem: 'PM' },
+ })
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(3)
+ await hoursInput.trigger('blur')
+ const emitted = wrapper.emitted('get-time')
+ const lastEmit = emitted![emitted!.length - 1][0] as any
+ expect(lastEmit.militaryTime).toBe(15)
+ })
+
+ it('emits correct militaryTime for AM hours', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(3)
+ await hoursInput.trigger('blur')
+ const emitted = wrapper.emitted('get-time')
+ const lastEmit = emitted![emitted!.length - 1][0] as any
+ expect(lastEmit.militaryTime).toBe(3)
+ })
+
+ it('emits correct militaryTime for 12 PM', async () => {
+ const wrapper = mount(ADateTime, {
+ props: { defaultHours: 12, defaultMeridiem: 'PM' },
+ })
+ const emitted = wrapper.emitted('get-time')
+ const lastEmit = emitted![emitted!.length - 1][0] as any
+ expect(lastEmit.militaryTime).toBe(12)
+ })
+
+ it('emits correct militaryTime for 12 AM', async () => {
+ const wrapper = mount(ADateTime, {
+ props: { defaultHours: 12, defaultMeridiem: 'AM' },
+ })
+ const emitted = wrapper.emitted('get-time')
+ const lastEmit = emitted![emitted!.length - 1][0] as any
+ expect(lastEmit.militaryTime).toBe(12)
+ })
+
+ it('selects input text on focus', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ const selectMock = vi.fn()
+ Object.defineProperty(hoursInput.element, 'select', { value: selectMock })
+ await hoursInput.trigger('focus')
+ expect(selectMock).toHaveBeenCalled()
+ })
+
+ it('handles paste on hours field', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ const clipboardData = { getData: vi.fn().mockReturnValue('143045') }
+ const event = new Event('paste', { bubbles: true, cancelable: true })
+ Object.defineProperty(event, 'clipboardData', { value: clipboardData })
+ Object.defineProperty(event, 'target', { value: hoursInput.element })
+ await hoursInput.element.dispatchEvent(event)
+ await wrapper.vm.$nextTick()
+ // After paste all fields, confirmTime should have run
+ const emitted = wrapper.emitted('get-time')
+ expect(emitted).toBeTruthy()
+ })
+
+ it('handles single field paste', async () => {
+ const wrapper = mount(ADateTime)
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ const clipboardData = { getData: vi.fn().mockReturnValue('55') }
+ const event = new Event('paste', { bubbles: true, cancelable: true })
+ Object.defineProperty(event, 'clipboardData', { value: clipboardData })
+ Object.defineProperty(event, 'target', { value: minutesInput.element })
+ await minutesInput.element.dispatchEvent(event)
+ await wrapper.vm.$nextTick()
+ })
+
+ it('pads single digit values on confirm', async () => {
+ const wrapper = mount(ADateTime)
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ await hoursInput.setValue(3)
+ await hoursInput.trigger('blur')
+ expect(hoursInput.element.value).toBe('03')
+ })
+
+ it('increments minutes when seconds roll over', async () => {
+ const wrapper = mount(ADateTime)
+ const secondsInput = wrapper.findAll('input[type="text"]')[2]
+ await secondsInput.setValue(59)
+ await secondsInput.trigger('keydown.up')
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ expect(minutesInput.element.value).toBe('01')
+ })
+
+ it('decrements minutes when seconds roll under', async () => {
+ const wrapper = mount(ADateTime)
+ const secondsInput = wrapper.findAll('input[type="text"]')[2]
+ await secondsInput.setValue(0)
+ await secondsInput.trigger('keydown.down')
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ expect(minutesInput.element.value).toBe('59')
+ })
+
+ it('increments hours when minutes roll over', async () => {
+ const wrapper = mount(ADateTime)
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ await minutesInput.setValue(59)
+ await minutesInput.trigger('keydown.up')
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ expect(hoursInput.element.value).toBe('01')
+ })
+
+ it('decrements hours when minutes roll under', async () => {
+ const wrapper = mount(ADateTime)
+ const minutesInput = wrapper.findAll('input[type="text"]')[1]
+ await minutesInput.setValue(0)
+ await minutesInput.trigger('keydown.down')
+ const hoursInput = wrapper.findAll('input[type="text"]')[0]
+ expect(hoursInput.element.value).toBe('11')
+ })
+})
diff --git a/common/autoinstallers/doc-tools/pnpm-lock.yaml b/common/autoinstallers/doc-tools/pnpm-lock.yaml
index 19f56500..7ea670c9 100644
--- a/common/autoinstallers/doc-tools/pnpm-lock.yaml
+++ b/common/autoinstallers/doc-tools/pnpm-lock.yaml
@@ -9,22 +9,22 @@ importers:
.:
dependencies:
'@microsoft/api-extractor-model':
- specifier: ^7.33.0
- version: 7.33.0
+ specifier: ^7.30.7
+ version: 7.30.7
packages:
- '@microsoft/api-extractor-model@7.33.0':
- resolution: {integrity: sha512-cMrvErE9yJz8aImpRztUfbO085WRSI4nsvMQ+VNGgHxiQO7s5LAXrt+B35RUghIsn0JdNdqIzusXXtKgSnXh7Q==}
+ '@microsoft/api-extractor-model@7.30.7':
+ resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==}
- '@microsoft/tsdoc-config@0.18.0':
- resolution: {integrity: sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw==}
+ '@microsoft/tsdoc-config@0.17.1':
+ resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==}
- '@microsoft/tsdoc@0.16.0':
- resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
+ '@microsoft/tsdoc@0.15.1':
+ resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
- '@rushstack/node-core-library@5.20.0':
- resolution: {integrity: sha512-yix/WFzuMPvbECgQjdzjDqynv7YQnrcGUfy56WU7QWAVcoN4uB1wCwpt3heo/ghHp2nINrRecPtVS7sQmqY+OA==}
+ '@rushstack/node-core-library@5.14.0':
+ resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
@@ -56,8 +56,8 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
- fs-extra@11.3.3:
- resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
+ fs-extra@11.3.2:
+ resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'}
function-bind@1.1.2:
@@ -102,8 +102,8 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
- resolve@1.22.11:
- resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
+ resolve@1.22.10:
+ resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
hasBin: true
@@ -128,32 +128,32 @@ packages:
snapshots:
- '@microsoft/api-extractor-model@7.33.0':
+ '@microsoft/api-extractor-model@7.30.7':
dependencies:
- '@microsoft/tsdoc': 0.16.0
- '@microsoft/tsdoc-config': 0.18.0
- '@rushstack/node-core-library': 5.20.0
+ '@microsoft/tsdoc': 0.15.1
+ '@microsoft/tsdoc-config': 0.17.1
+ '@rushstack/node-core-library': 5.14.0
transitivePeerDependencies:
- '@types/node'
- '@microsoft/tsdoc-config@0.18.0':
+ '@microsoft/tsdoc-config@0.17.1':
dependencies:
- '@microsoft/tsdoc': 0.16.0
+ '@microsoft/tsdoc': 0.15.1
ajv: 8.12.0
jju: 1.4.0
- resolve: 1.22.11
+ resolve: 1.22.10
- '@microsoft/tsdoc@0.16.0': {}
+ '@microsoft/tsdoc@0.15.1': {}
- '@rushstack/node-core-library@5.20.0':
+ '@rushstack/node-core-library@5.14.0':
dependencies:
ajv: 8.13.0
ajv-draft-04: 1.0.0(ajv@8.13.0)
ajv-formats: 3.0.1(ajv@8.13.0)
- fs-extra: 11.3.3
+ fs-extra: 11.3.2
import-lazy: 4.0.0
jju: 1.4.0
- resolve: 1.22.11
+ resolve: 1.22.10
semver: 7.5.4
ajv-draft-04@1.0.0(ajv@8.13.0):
@@ -180,7 +180,7 @@ snapshots:
fast-deep-equal@3.1.3: {}
- fs-extra@11.3.3:
+ fs-extra@11.3.2:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.0
@@ -220,7 +220,7 @@ snapshots:
require-from-string@2.0.2: {}
- resolve@1.22.11:
+ resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
diff --git a/common/reviews/api/aform.api.md b/common/reviews/api/aform.api.md
index 12de97f1..878f5c69 100644
--- a/common/reviews/api/aform.api.md
+++ b/common/reviews/api/aform.api.md
@@ -8,6 +8,8 @@ import ACheckbox from './components/form/ACheckbox.vue';
import AComboBox from './components/form/AComboBox.vue';
import ADate from './components/form/ADate.vue';
import ADatePicker from './components/form/ADatePicker.vue';
+import ADateSelection from './components/form/ADateSelection.vue';
+import ADateTime from './components/form/ADateTime.vue';
import ADropdown from './components/form/ADropdown.vue';
import AFieldset from './components/form/AFieldset.vue';
import AFileAttach from './components/form/AFileAttach.vue';
@@ -28,6 +30,10 @@ export { ADate }
export { ADatePicker }
+export { ADateSelection }
+
+export { ADateTime }
+
export { ADropdown }
export { AFieldset }
@@ -51,6 +57,7 @@ export type BaseSchema = {
export type ComponentProps = {
schema?: SchemaTypes;
label?: string;
+ selectRange?: boolean;
mask?: string;
required?: boolean;
mode?: FormMode;
diff --git a/docs/reference/aform.md b/docs/reference/aform.md
index 9fb80510..aedfdf9f 100644
--- a/docs/reference/aform.md
+++ b/docs/reference/aform.md
@@ -41,6 +41,22 @@ Vue component exported from @stonecrop/aform.
import { ADatePicker } from '@stonecrop/aform'
```
+### ADateSelection
+
+Vue component exported from @stonecrop/aform.
+
+```typescript
+import { ADateSelection } from '@stonecrop/aform'
+```
+
+### ADateTime
+
+Vue component exported from @stonecrop/aform.
+
+```typescript
+import { ADateTime } from '@stonecrop/aform'
+```
+
### ADropdown
Vue component exported from @stonecrop/aform.
@@ -141,6 +157,7 @@ Defined props for AForm components
export type ComponentProps = {
schema?: SchemaTypes;
label?: string;
+ selectRange?: boolean;
mask?: string;
required?: boolean;
mode?: FormMode;
diff --git a/examples/aform/date.story.vue b/examples/aform/date.story.vue
index c11bb11a..ba4622eb 100644
--- a/examples/aform/date.story.vue
+++ b/examples/aform/date.story.vue
@@ -1,21 +1,69 @@
-
+
+
+
+
+ (state.militaryTime = !state.militaryTime)"
+ v-model="state.militaryTime"
+ title="Military Time" />
+
+
+
- Default Date
-
-
- Custom Date
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+ (state.militaryTime = !state.militaryTime)"
+ v-model="state.militaryTime"
+ title="Military Time" />
+
+
diff --git a/examples/atable/default.story.vue b/examples/atable/default.story.vue
index fd9b4f5b..d75c18f4 100644
--- a/examples/atable/default.story.vue
+++ b/examples/atable/default.story.vue
@@ -97,7 +97,7 @@ const columns: TableColumn[] = [
align: 'center',
edit: true,
width: '25ch',
- modalComponent: 'DateInput',
+ modalComponent: 'ADateSelection',
format: (value: number) => new Date(value).toLocaleDateString('en-US'),
},
]
@@ -127,7 +127,7 @@ const readonly_columns: TableColumn[] = [
align: 'center',
edit: false,
width: '25ch',
- modalComponent: 'DateInput',
+ modalComponent: 'ADateSelection',
modalComponentExtraProps: { mode: 'read' },
format: (value: number) => new Date(value).toLocaleDateString('en-US'),
},
diff --git a/examples/atable/list.story.vue b/examples/atable/list.story.vue
index 505c975d..18c3aa3b 100644
--- a/examples/atable/list.story.vue
+++ b/examples/atable/list.story.vue
@@ -210,7 +210,7 @@ const http_logs = ref({
align: 'center',
edit: true,
width: '25ch',
- modalComponent: 'DateInput',
+ modalComponent: 'ADateSelection',
format: (value: number) => new Date(value).toLocaleDateString('en-US'),
},
] as TableColumn[],
@@ -272,7 +272,7 @@ const pinned_extra_logs = ref({
edit: true,
width: '25ch',
pinned: false,
- modalComponent: 'DateInput',
+ modalComponent: 'ADateSelection',
format: (value: string | number) => new Date(value).toLocaleDateString('en-US'),
},
{
@@ -310,7 +310,7 @@ const pinned_extra_logs = ref({
edit: true,
width: '25ch',
pinned: false,
- modalComponent: 'DateInput',
+ modalComponent: 'ADateSelection',
format: (value: string | number) => new Date(value).toLocaleDateString('en-US'),
},
{