mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-03-01 12:44:28 +00:00
120 lines
3.8 KiB
Vue
120 lines
3.8 KiB
Vue
<script lang="ts">
|
|
import { defineComponent, getCurrentInstance, h, isVue2 } from 'vue-demi'
|
|
|
|
import { cn } from '#src/utils'
|
|
|
|
import { ICON_SIZES } from './icon.variants'
|
|
import { SYSTEM_ICONS } from './icons'
|
|
|
|
import type { Size } from '#src/types'
|
|
import type { SystemIconName } from './icons'
|
|
import type { ClassValue } from 'clsx'
|
|
import type { Component, PropType } from 'vue-demi'
|
|
|
|
/**
|
|
* Renders system icons by name or custom Vue components.
|
|
* @prop name - System icon name (e.g. 'close', 'check')
|
|
* @prop icon - Vue component to render as icon (takes precedence over name)
|
|
* @prop size - Icon size (xs/sm/md/lg/xl/2xl)
|
|
*/
|
|
export default defineComponent({
|
|
name: 'OsIcon',
|
|
inheritAttrs: false,
|
|
props: {
|
|
name: {
|
|
type: String,
|
|
default: undefined,
|
|
},
|
|
icon: {
|
|
type: [Object, Function] as PropType<Component>,
|
|
default: undefined,
|
|
},
|
|
size: {
|
|
type: String as PropType<Size>,
|
|
default: 'md',
|
|
},
|
|
},
|
|
setup(props, { attrs }) {
|
|
/* v8 ignore start -- Vue 2 only */
|
|
const instance = isVue2 ? getCurrentInstance() : null
|
|
/* v8 ignore stop */
|
|
|
|
return () => {
|
|
// icon prop takes precedence over name
|
|
const iconComponent =
|
|
props.icon || (props.name ? SYSTEM_ICONS[props.name as SystemIconName] : undefined)
|
|
|
|
if (!iconComponent) return null
|
|
|
|
const sizeClass = ICON_SIZES[props.size]
|
|
|
|
// Vue 2's h() cannot handle plain arrow functions as components (only
|
|
// constructor functions or option objects). SYSTEM_ICONS entries are
|
|
// arrow functions that return VNodes, so call them directly.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const isRenderFn = typeof iconComponent === 'function' && !(iconComponent as any).cid
|
|
const iconVNode = isRenderFn
|
|
? (iconComponent as () => ReturnType<typeof h>)()
|
|
: h(iconComponent)
|
|
|
|
/* v8 ignore start -- Vue 2 branch tested in webapp Jest tests */
|
|
if (isVue2) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const proxy = instance?.proxy as any
|
|
const parentClass = proxy?.$vnode?.data?.staticClass || ''
|
|
const parentDynClass = proxy?.$vnode?.data?.class
|
|
const parentAttrs = proxy?.$vnode?.data?.attrs || {}
|
|
|
|
const hasLabel = !!(parentAttrs['aria-label'] || attrs['aria-label'])
|
|
const a11yAttrs = hasLabel
|
|
? { role: 'img', 'aria-label': parentAttrs['aria-label'] || attrs['aria-label'] }
|
|
: { 'aria-hidden': 'true' }
|
|
|
|
return h(
|
|
'span',
|
|
{
|
|
class: [
|
|
cn(
|
|
'os-icon inline-flex items-center shrink-0',
|
|
sizeClass,
|
|
'[&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current',
|
|
),
|
|
parentClass,
|
|
parentDynClass,
|
|
].filter(Boolean),
|
|
attrs: { ...a11yAttrs, ...parentAttrs, ...attrs },
|
|
},
|
|
[iconVNode],
|
|
)
|
|
}
|
|
/* v8 ignore stop */
|
|
|
|
const {
|
|
class: attrClass,
|
|
'aria-label': ariaLabel,
|
|
...restAttrs
|
|
} = attrs as Record<string, unknown>
|
|
const hasLabel = !!ariaLabel
|
|
const a11yAttrs = hasLabel
|
|
? { role: 'img', 'aria-label': ariaLabel }
|
|
: { 'aria-hidden': 'true' }
|
|
|
|
return h(
|
|
'span',
|
|
{
|
|
class: cn(
|
|
'os-icon inline-flex items-center shrink-0',
|
|
sizeClass,
|
|
'[&>svg]:h-full [&>svg]:w-auto [&>svg]:fill-current',
|
|
attrClass as ClassValue,
|
|
),
|
|
...a11yAttrs,
|
|
...restAttrs,
|
|
},
|
|
[iconVNode],
|
|
)
|
|
}
|
|
},
|
|
})
|
|
</script>
|