diff --git a/docs/components.md b/docs/components.md index b3755d4..3396852 100644 --- a/docs/components.md +++ b/docs/components.md @@ -10,6 +10,7 @@ - [Header](../src/components/Header//README.md) - [Input](../src/components/Input//README.md) - [InputDatePicker](../src/components/InputDatePicker//README.md) +- [InputRadio](../src/components/InputRadio//README.md) - [InputSelect](../src/components/InputSelect//README.md) - [Nav](../src/components/Nav//README.md) - [QuickSearchForm](../src/components/QuickSearchForm//README.md) diff --git a/src/components/InputRadio/PdapInputRadio.vue b/src/components/InputRadio/PdapInputRadio.vue new file mode 100644 index 0000000..075538c --- /dev/null +++ b/src/components/InputRadio/PdapInputRadio.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/components/InputRadio/README.md b/src/components/InputRadio/README.md new file mode 100644 index 0000000..7c1d9e4 --- /dev/null +++ b/src/components/InputRadio/README.md @@ -0,0 +1,53 @@ +# InputRadio +Radio input. Designed to be wrapped with `RadioGroup` + +## Props - required + +| name | required? | types | description | default | +| ---------------- | ----------------------------- | --------- | --------------------------------------------- | ------- | +| `defaultChecked` | no | `boolean` | radio is checked by default. Only 1 per group | | +| `id` | yes | `string` | id attr | | +| `label` | yes, if label slot not passed | `string` | label content | | +| `name` | yes | `string` | name attr | | + +## Slots + +| name | required? | types | description | default | +| ------- | ----------------------------- | --------- | ------------------------------------ | ------- | +| `label` | yes, if label prop not passed | `Element` | slot content to be rendered as label | | + +## Example + +```vue + + + + +... +``` diff --git a/src/components/InputRadio/index.ts b/src/components/InputRadio/index.ts new file mode 100644 index 0000000..e8e5c4c --- /dev/null +++ b/src/components/InputRadio/index.ts @@ -0,0 +1 @@ +export { default as InputRadio } from './PdapInputRadio.vue'; diff --git a/src/components/InputRadio/input-radio.spec.ts b/src/components/InputRadio/input-radio.spec.ts new file mode 100644 index 0000000..a7d00a2 --- /dev/null +++ b/src/components/InputRadio/input-radio.spec.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import PdapInputRadio from './PdapInputRadio.vue'; +import { provideKey } from '../FormV2/util'; +import { ref } from 'vue'; + +describe('PdapInputRadio', () => { + let wrapper: VueWrapper; + const mockSetValues = vi.fn(); + const mockValues = ref({}); + const mockV$ = ref({ + testName: { + $error: false, + $errors: [], + }, + }); + + const defaultProps = { + name: 'testName', + id: 'test-id', + label: 'Test Label', + value: 'test-value', + }; + + beforeEach(() => { + mockSetValues.mockClear(); + }); + + const createWrapper = (props = {}, provide = {}, slots = {}) => { + return mount(PdapInputRadio, { + props: { + ...defaultProps, + ...props, + }, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + ...provide, + }, + }, + }, + slots: { ...slots }, + }); + }; + + it('renders correctly with default props', () => { + wrapper = createWrapper(); + expect(wrapper.find('input[type="radio"]').exists()).toBe(true); + expect(wrapper.find('label').text()).toBe('Test Label'); + }); + + it('renders with slot label instead of prop label', () => { + wrapper = createWrapper( + {}, + {}, + { + label: 'Slot Label', + } + ); + expect(wrapper.find('label span').text()).toBe('Slot Label'); + }); + + it('throws error when no label passed as slot or prop', async () => { + expect(() => { + wrapper = createWrapper({ label: undefined }, {}, {}); + }).toThrow('All form inputs must have a label, passed as a slot or a prop'); + }); + + it('emits input event and calls setValues when changed', async () => { + wrapper = createWrapper(); + const input = wrapper.find('input'); + await input.setValue(true); + expect(mockSetValues).toHaveBeenCalledWith({ + [defaultProps.name]: defaultProps.value, + }); + }); + + it('renders with defaultChecked prop', () => { + wrapper = createWrapper({ defaultChecked: true }); + expect(wrapper.find('input').element.defaultChecked).toBe(true); + }); +}); diff --git a/src/components/InputRadio/types.ts b/src/components/InputRadio/types.ts new file mode 100644 index 0000000..723264b --- /dev/null +++ b/src/components/InputRadio/types.ts @@ -0,0 +1,7 @@ +export interface PdapInputRadioProps { + id: string; + label?: string; + name: string; + defaultChecked?: boolean; + value: string; +} diff --git a/src/components/InputRadioGroup/PdapInputRadioGroup.vue b/src/components/InputRadioGroup/PdapInputRadioGroup.vue new file mode 100644 index 0000000..6362f0b --- /dev/null +++ b/src/components/InputRadioGroup/PdapInputRadioGroup.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/InputRadioGroup/index.ts b/src/components/InputRadioGroup/index.ts new file mode 100644 index 0000000..0949494 --- /dev/null +++ b/src/components/InputRadioGroup/index.ts @@ -0,0 +1 @@ +export { default as RadioGroup } from './PdapInputRadioGroup.vue'; diff --git a/src/components/InputRadioGroup/input-radio-group.spec.ts b/src/components/InputRadioGroup/input-radio-group.spec.ts new file mode 100644 index 0000000..95c2b4e --- /dev/null +++ b/src/components/InputRadioGroup/input-radio-group.spec.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import RadioGroup from './PdapInputRadioGroup.vue'; +import { provideKey } from '../FormV2/util'; +import { ref } from 'vue'; + +describe('RadioGroup', () => { + let wrapper: VueWrapper; + const mockSetValues = vi.fn(); + const mockValues = ref({}); + const mockV$ = ref({ + testName: { + $error: false, + $errors: [], + }, + }); + + const defaultProps = { + name: 'testName', + }; + + beforeEach(() => { + mockSetValues.mockClear(); + }); + + const createWrapper = (props = {}, slots = {}) => { + return mount(RadioGroup, { + props: { + ...defaultProps, + ...props, + }, + slots, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + }, + }); + }; + + it('renders correctly with default props', () => { + wrapper = createWrapper(); + expect(wrapper.find('.pdap-input-radio-group').exists()).toBe(true); + }); + + it('renders slot content', () => { + wrapper = createWrapper( + {}, + { + default: '
Test Content
', + } + ); + expect(wrapper.find('.test-content').exists()).toBe(true); + }); + + it('shows error message when error exists', async () => { + mockV$.value.testName.$error = true; + // @ts-expect-error + mockV$.value.testName.$errors = [{ $message: 'Test error message' }]; + wrapper = createWrapper(); + expect(wrapper.find('.pdap-input-error').exists()).toBe(true); + }); + + it('shows custom error slot when provided and error exists', () => { + mockV$.value.testName.$error = true; + wrapper = createWrapper( + {}, + { + error: '
Custom Error
', + } + ); + expect(wrapper.find('.custom-error').exists()).toBe(true); + }); +}); diff --git a/src/components/InputSelect/PdapInputSelect.vue b/src/components/InputSelect/PdapInputSelect.vue index 3fbfec5..bf4e548 100644 --- a/src/components/InputSelect/PdapInputSelect.vue +++ b/src/components/InputSelect/PdapInputSelect.vue @@ -39,7 +39,7 @@ />
@@ -168,25 +168,55 @@ function handleClick() { else toggleOpen(); } -function handleKeyDown(event: KeyboardEvent) { - if (event.key === 'Tab') { - if ( - !event.shiftKey && - focusedOptionIndex.value === filteredOptions.value.length - 1 - ) { - event.preventDefault(); - return; - } - - if (event.shiftKey && focusedOptionIndex.value === 0) { - event.preventDefault(); - if (combobox) isOpen.value = false; - else closeAndReturnFocus(); - return; - } +// function handleKeyUp(event: KeyboardEvent) { +// if (event.key === 'Tab') { +// if ( +// !event.shiftKey && +// focusedOptionIndex.value === filteredOptions.value.length - 1 +// ) { +// event.preventDefault(); +// return; +// } + +// if (event.shiftKey) { +// if (isOpen.value) { +// if (focusedOptionIndex.value === -1) { +// isOpen.value = false; +// } +// if (focusedOptionIndex.value === 0) { +// event.preventDefault(); +// closeAndReturnFocus(); +// } else { +// event.preventDefault(); +// focusedOptionIndex.value = focusedOptionIndex.value - 1; +// } + +// return; +// } +// } +// } +// } - return; - } +function handleKeyDown(event: KeyboardEvent) { + // if (event.key === 'Tab') { + // if ( + // !event.shiftKey && + // focusedOptionIndex.value === filteredOptions.value.length - 1 + // ) { + // event.preventDefault(); + // return; + // } + + // if (event.shiftKey && focusedOptionIndex.value === 0) { + // event.preventDefault(); + // closeAndReturnFocus(); + // } else { + // event.preventDefault(); + // focusedOptionIndex.value = focusedOptionIndex.value - 1; + // } + + // return; + // } if (!isOpen.value) { if (['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) { diff --git a/src/demo/pages/FormV2Demo.vue b/src/demo/pages/FormV2Demo.vue index 991ad50..2a71883 100644 --- a/src/demo/pages/FormV2Demo.vue +++ b/src/demo/pages/FormV2Demo.vue @@ -27,8 +27,6 @@ label="Type your password here" /> -

Foo bar baz, extra content here

- + +

+ Select another flavor, with radio buttons this time! +

+ +
+