Skip to content

Commit

Permalink
feat(web-core): new VtsLink component
Browse files Browse the repository at this point in the history
  • Loading branch information
ByScripts committed Sep 20, 2024
1 parent 02ed1bc commit ce0408a
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 0 deletions.
41 changes: 41 additions & 0 deletions @xen-orchestra/lite/src/stories/web-core/vts-link.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<template>
<ComponentStory
v-slot="{ properties }"
:params="[
prop('accent').required().enum('brand', 'success', 'warning', 'danger').preset('brand').widget(),
prop('size').required().enum('small', 'medium').preset('medium').widget(),
prop('to').type('IconDefinition').widget(text()),
prop('href').str().widget(),
iconProp(),
prop('disabled').bool().widget(),
prop('target').enum('_blank', '_self').widget(),
]"
>
<VtsLink v-bind="properties">This is a link</VtsLink>

<div v-if="!properties.to && !properties.href" class="info">
<UiIcon :icon="faInfoCircle" color="normal" />
Link is disabled because no `href` or `to` is provided
</div>
<div v-else-if="properties.to && properties.href" class="info">
<UiIcon :icon="faExclamationTriangle" color="warning" />
`to` is ignored when `href` is provided
</div>
</ComponentStory>
</template>

<script lang="ts" setup>
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { iconProp, prop } from '@/libs/story/story-param'
import { text } from '@/libs/story/story-widget'
import UiIcon from '@core/components/icon/UiIcon.vue'
import VtsLink from '@core/components/link/VtsLink.vue'
import { faExclamationTriangle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'
</script>

<style lang="postcss" scoped>
.info {
margin-top: 4rem;
font-style: italic;
}
</style>
137 changes: 137 additions & 0 deletions @xen-orchestra/web-core/lib/components/link/VtsLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<component :is="component" :class="classes" class="vts-link" v-bind="attributes">
<UiIcon :icon="icon" color="current" />
<slot />
<UiIcon
v-if="attributes.target === '_blank'"
:icon="faArrowUpRightFromSquare"
class="external-icon"
color="current"
/>
</component>
</template>

<script lang="ts" setup>
import UiIcon from '@core/components/icon/UiIcon.vue'
import { useLinkComponent } from '@core/composables/link-component.composable'
import { toVariants } from '@core/utils/to-variants.util'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
const props = defineProps<{
size: 'small' | 'medium'
accent: 'brand' | 'success' | 'warning' | 'danger'
icon?: IconDefinition
disabled?: boolean
href?: string
to?: RouteLocationRaw
target?: '_blank' | '_self'
}>()
const typoClasses = {
small: 'typo p3-regular',
medium: 'typo p1-regular',
}
const { component, attributes, isDisabled } = useLinkComponent('span', () => props)
const classes = computed(() => [
typoClasses[props.size],
toVariants({
disabled: isDisabled.value,
accent: props.accent,
}),
])
</script>

<style lang="postcss" scoped>
/*
ACCENT + STATE
--vts-link--color
*/
.vts-link {
&.accent--brand {
--vts-link--color: var(--color-normal-txt-base);
&:hover {
--vts-link--color: var(--color-normal-txt-hover);
}
&:active {
--vts-link--color: var(--color-normal-txt-active);
}
&.disabled {
--vts-link--color: var(--color-neutral-txt-secondary);
}
}
&.accent--success {
--vts-link--color: var(--color-success-txt-base);
&:hover {
--vts-link--color: var(--color-success-txt-hover);
}
&:active {
--vts-link--color: var(--color-success-txt-active);
}
&.disabled {
--vts-link--color: var(--color-neutral-txt-secondary);
}
}
&.accent--warning {
--vts-link--color: var(--color-warning-txt-base);
&:hover {
--vts-link--color: var(--color-warning-txt-hover);
}
&:active {
--vts-link--color: var(--color-warning-txt-active);
}
&.disabled {
--vts-link--color: var(--color-neutral-txt-secondary);
}
}
&.accent--danger {
--vts-link--color: var(--color-danger-txt-base);
&:hover {
--vts-link--color: var(--color-danger-txt-hover);
}
&:active {
--vts-link--color: var(--color-danger-txt-active);
}
&.disabled {
--vts-link--color: var(--color-neutral-txt-secondary);
}
}
}
/* IMPLEMENTATION */
.vts-link {
display: inline-flex;
align-items: center;
gap: 0.8rem;
color: var(--vts-link--color);
text-decoration: underline;
text-underline-offset: 0.2rem;
&.disabled {
cursor: not-allowed;
}
.external-icon {
font-size: 0.75em;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { MaybeRefOrGetter } from 'vue'
import { computed, toValue } from 'vue'
import type { RouteLocationRaw } from 'vue-router'

type Options = {
to?: RouteLocationRaw
href?: string
target?: '_blank' | '_self'
disabled?: boolean
}

export function useLinkComponent(defaultComponent: string, options: MaybeRefOrGetter<Options>) {
const config = computed(() => toValue(options))

const isDisabled = computed(() => config.value.disabled || (!config.value.to && !config.value.href))

const component = computed(() => {
if (isDisabled.value) {
return defaultComponent
}

if (config.value.href) {
return 'a'
}

return 'RouterLink'
})

const attributes = computed(() => {
if (isDisabled.value) {
return {}
}

if (config.value.href) {
return {
rel: 'noopener noreferrer',
target: config.value.target ?? '_blank',
href: config.value.href,
}
}

return {
target: config.value.target,
to: config.value.to,
}
})

return {
isDisabled,
component,
attributes,
}
}

0 comments on commit ce0408a

Please sign in to comment.