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
+
+ onSubmit({ values })"
+ @change="(values, event) => onChange({ values, event })"
+ >
+
+
+
+ Select another flavor, with radio buttons this time!
+
+
+
+
+
+
+
+
+...
+```
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!
+
+
+
+