diff --git a/package-lock.json b/package-lock.json index 21f7769..a49392c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "jsdom": "^26.1.0", "primevue": "^4.3.6", "vite-plugin-inspect": "^11.3.0", - "vue": "^3.5.17" + "vue": "^3.5.17", + "vue-router": "^4.5.1" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", @@ -2424,6 +2425,12 @@ "he": "^1.2.0" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/@vue/devtools-core": { "version": "7.7.7", "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.7.tgz", @@ -6305,6 +6312,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/vue-tsc": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", diff --git a/package.json b/package.json index 15bfe1f..aa23439 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "jsdom": "^26.1.0", "primevue": "^4.3.6", "vite-plugin-inspect": "^11.3.0", - "vue": "^3.5.17" + "vue": "^3.5.17", + "vue-router": "^4.5.1" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", diff --git a/src/App.vue b/src/App.vue index ac826ea..c7eecad 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,10 +1,12 @@ diff --git a/src/assets/style/primevue-style/button.css b/src/assets/style/primevue-style/button.css index 6d8fb80..f281ec1 100644 --- a/src/assets/style/primevue-style/button.css +++ b/src/assets/style/primevue-style/button.css @@ -64,4 +64,8 @@ --p-button-primary-focus-ring-color: var(--focus-color); --p-button-secondary-focus-ring-color: var(--focus-color); --p-button-danger-focus-ring-color: var(--focus-color); + /* link */ + --p-button-link-color: var(--text-action-high-blue-france); + --p-button-link-hover-color: var(--text-action-high-blue-france); + --p-button-link-active-color: var(--text-action-high-blue-france); } diff --git a/src/components/button/IVLink.type.ts b/src/components/button/IVLink.type.ts new file mode 100644 index 0000000..088230c --- /dev/null +++ b/src/components/button/IVLink.type.ts @@ -0,0 +1,46 @@ +import type { RouteLocationRaw } from "vue-router"; + +/** + * Interface representing a navigational link component. + * This can be used for both internal routing (via `to`) and external links (via `href`). + */ +export default interface IVLink { + /** + * The text label displayed for the link. + */ + label: string; + + /** + * Internal route destination using Vue Router's RouteLocationRaw. + * Optional – used for client-side navigation. + */ + to?: RouteLocationRaw; + + /** + * External URL for the link. + * Optional – used for standard anchor navigation. + */ + href?: string; + + /** + * Optional icon name or path to be displayed alongside the label. + */ + icon?: string; + + /** + * Specifies where to open the linked document (e.g., "_blank", "_self"). + * Optional – applies to external links. + */ + target?: string; + + /** + * If true, the link is disabled and not clickable. + */ + disabled?: boolean; + + /** + * If true, the icon is displayed on the right side of the label. + * Defaults to false (icon on the left). + */ + iconRight?: boolean; +} diff --git a/src/components/button/VLink.vue b/src/components/button/VLink.vue new file mode 100644 index 0000000..c20a6c5 --- /dev/null +++ b/src/components/button/VLink.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/test/VLink.spec.ts b/test/VLink.spec.ts new file mode 100644 index 0000000..f8b2578 --- /dev/null +++ b/test/VLink.spec.ts @@ -0,0 +1,58 @@ +import { mount } from '@vue/test-utils' +import VLink from '../src/components/button/VLink.vue' +import {test, expect, describe, vi} from 'vitest' + +describe('VLink', () => { + test('renders the label correctly', () => { + const wrapper = mount(VLink, { + props: { + label: 'Link' + } + }) + expect(wrapper.text()).toContain('Link'); + }) + + test('renders as an anchor tag when `href` is provided', () => { + const wrapper = mount(VLink, { + props: {label: 'External', href: 'https://example.com' } + }); + + const a = wrapper.find('a') + expect(a.exists()).toBe(true); + expect(a.attributes('href')).toBe('https://example.com'); + }) + + test('disables the link when `disabled` is true', () => { + const wrapper = mount(VLink, { + props: {label: 'Disabled', disabled: true } + }) + + const button = wrapper.find('.p-button'); + expect(button.classes()).toContain('disabled'); + expect(button.attributes('aria-disabled')).toBe('true'); + }); + + test('prevents click when disabled', async () => { + const clickHandler = vi.fn(); + const wrapper = mount(VLink, { + props: {label: 'Disabled', disabled: true, onClick: clickHandler} + }) + + await wrapper.trigger('click') + expect(clickHandler).not.toHaveBeenCalled(); + }); + + test('places icon on the right when `iconRight` is true', () => { + const wrapper = mount(VLink, { + props: { + label: 'link', + icon: "ri-external-link-line", + iconRight: true, + } + }) + const icon = wrapper.find('.p-button-icon-right'); + expect(icon.exists()).toBe(true); + expect(icon.classes()).toContain('ri-external-link-line'); + }) +}) +