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 @@ 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 }} > + + +
+ +
-
+ +
+ + + M T @@ -26,15 +50,20 @@ v-for="colNo in numberOfColumns" ref="celldate" :key="getCurrentCell(rowNo, colNo)" + class="date-cell" :contenteditable="false" :spellcheck="false" :tabindex="0" :class="{ todaysDate: isTodaysDate(getCurrentDate(rowNo, colNo)), selectedDate: isSelectedDate(getCurrentDate(rowNo, colNo)), + withinRange: selectRange ? isInDateRange(getCurrentDate(rowNo, colNo)) : false, + startDate: selectRange ? isStartDate(getCurrentDate(rowNo, colNo)) : false, + endDate: selectRange ? isEndDate(getCurrentDate(rowNo, colNo)) : false, }" @click.prevent.stop="selectDate(getCurrentCell(rowNo, colNo))" - @keydown.enter="selectDate(getCurrentCell(rowNo, colNo))"> + @keydown.enter="selectDate(getCurrentCell(rowNo, colNo))" + @mouseover="hoverDate(getCurrentCell(rowNo, colNo))"> {{ new Date(getCurrentDate(rowNo, colNo)).getDate() }} @@ -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 @@ 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'), }, {