Merge branch 'link' into 'main'

Link

See merge request cellule-financiere-pmo/design-system/visua-vue!3
This commit is contained in:
Paul Valerie GOMA 2025-07-21 11:15:02 +00:00
commit 475240dcf1
7 changed files with 197 additions and 4 deletions

24
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -1,10 +1,12 @@
<script setup lang="ts">
// import VButtonView from '../template/VButtonView.vue'
import VButtonGroupView from '../template/VButtonGroupView.vue';
// import VButtonGroupView from '../template/VButtonGroupView.vue';
import VLinkView from '../template/VLinkView.vue';
</script>
<template>
<!-- <VButtonView/> -->
<VButtonGroupView/>
<!-- <VButtonGroupView/> -->
<VLinkView/>
</template>

View File

@ -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);
}

View File

@ -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;
}

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import Button from 'primevue/button';
import type IVLink from '@/components/button/IVLink.type';
import { computed } from 'vue';
import styles from '@visua/typography.module.css';
const props = defineProps<IVLink>();
// Icon position computed
const iconPos = computed<string>(() => {
return props.iconRight ? 'right': 'left'
})
// Change the html tag of root element
const htmlTag = computed(() => { return props.to ? 'RouterLink' : props.href ? 'a' : 'button'})
</script>
<template>
<Button
variant="link"
role="link"
:label="props.label"
:aria-label="props.label"
:icon="props.icon"
:disabled="props.disabled"
:aria-disabled="props.disabled"
:icon-pos="iconPos"
:as="htmlTag"
:href="props.href"
:target="props.target"
:to="props.to"
:tabindex="props.disabled ? -1 : 0"
@click="props.disabled && $event.preventDefault()"
class="p-button"
:class="[styles['text-body-MD-standard-text-Regular'], {'disabled': props.disabled}]"
:style="props.disabled ? { pointerEvents: 'none' } : {}"
/>
</template>
<style lang="css" scoped>
*{
text-decoration: none;
margin: 0px;
padding: 0px;
}
.p-button{
padding: 0px;
gap: calc(var(--p-button-gap)/2);
}
.p-button.disabled{
--p-button-link-color: var(--text-disabled-grey);
--p-button-link-hover-color: var(--text-disabled-grey);
--p-button-link-active-color: var(--text-disabled-grey);
-webkit-user-select: none;
user-select: none;
}
</style>

58
test/VLink.spec.ts Normal file
View File

@ -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');
})
})