mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-04-03 08:05:33 +00:00
feat(package/ui): os-counter-icon (#9471)
This commit is contained in:
parent
e144170cf0
commit
202c869515
@ -2,11 +2,11 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('I see no unread chat messages in the header', () => {
|
||||
cy.get('.chat-notification-menu:visible', { timeout: 15000 }).should('exist')
|
||||
cy.get('.chat-notification-menu:visible .count.--danger').should('not.exist')
|
||||
cy.get('.chat-notification-menu:visible .os-counter-icon__count.os-counter-icon__count--danger').should('not.exist')
|
||||
})
|
||||
|
||||
defineStep('I see {int} unread chat message in the header', (count) => {
|
||||
cy.get('.chat-notification-menu:visible .count.--danger', { timeout: 15000 }).should(
|
||||
cy.get('.chat-notification-menu:visible .os-counter-icon__count.os-counter-icon__count--danger', { timeout: 15000 }).should(
|
||||
'contain',
|
||||
count,
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
|
||||
|
||||
defineStep('the unread counter is removed', () => {
|
||||
cy.get('.notifications-menu .counter-icon .count')
|
||||
cy.get('.notifications-menu .os-counter-icon .os-counter-icon__count')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { markRaw } from 'vue-demi'
|
||||
|
||||
import { IconCheck } from '#src/components/OsIcon'
|
||||
|
||||
import OsCounterIcon from './OsCounterIcon.vue'
|
||||
|
||||
const icon = markRaw(IconCheck)
|
||||
|
||||
describe('osCounterIcon', () => {
|
||||
const defaultProps = { icon, count: 5 }
|
||||
|
||||
it('renders with wrapper class', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: defaultProps })
|
||||
|
||||
expect(wrapper.classes()).toContain('os-counter-icon')
|
||||
})
|
||||
|
||||
it('renders the icon', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: defaultProps })
|
||||
|
||||
expect(wrapper.find('svg').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('displays the count badge when count > 0', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: defaultProps })
|
||||
|
||||
expect(wrapper.find('.os-counter-icon__count').text()).toBe('5')
|
||||
})
|
||||
|
||||
it('hides the count badge when count is 0', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: { icon, count: 0 } })
|
||||
|
||||
expect(wrapper.find('.os-counter-icon__count').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('caps count at 99+', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: { icon, count: 150 } })
|
||||
|
||||
expect(wrapper.find('.os-counter-icon__count').text()).toBe('99+')
|
||||
})
|
||||
|
||||
it('shows 99 without cap', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: { icon, count: 99 } })
|
||||
|
||||
expect(wrapper.find('.os-counter-icon__count').text()).toBe('99')
|
||||
})
|
||||
|
||||
describe('variants', () => {
|
||||
it('applies danger class', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: { ...defaultProps, danger: true } })
|
||||
|
||||
expect(wrapper.find('.os-counter-icon__count--danger').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('applies soft class', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: { ...defaultProps, soft: true } })
|
||||
|
||||
expect(wrapper.find('.os-counter-icon__count--soft').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('soft takes precedence over danger', () => {
|
||||
const wrapper = mount(OsCounterIcon, {
|
||||
props: { ...defaultProps, soft: true, danger: true },
|
||||
})
|
||||
|
||||
expect(wrapper.find('.os-counter-icon__count--soft').exists()).toBe(true)
|
||||
expect(wrapper.find('.os-counter-icon__count--danger').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('keyboard accessibility', () => {
|
||||
it('renders as non-interactive span (decorative element)', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: defaultProps })
|
||||
|
||||
expect((wrapper.element as HTMLElement).tagName).toBe('SPAN')
|
||||
})
|
||||
|
||||
it('is not focusable', () => {
|
||||
const wrapper = mount(OsCounterIcon, { props: defaultProps })
|
||||
|
||||
expect(wrapper.attributes('tabindex')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,63 @@
|
||||
import { ocelotIcons } from '#src/ocelot/icons'
|
||||
|
||||
import OsCounterIcon from './OsCounterIcon.vue'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
const iconMap = ocelotIcons
|
||||
const iconNames = Object.keys(iconMap)
|
||||
|
||||
const meta: Meta<typeof OsCounterIcon> = {
|
||||
title: 'Ocelot/CounterIcon',
|
||||
component: OsCounterIcon,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
icon: {
|
||||
control: 'select',
|
||||
options: iconNames,
|
||||
mapping: iconMap,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof OsCounterIcon>
|
||||
|
||||
export const Playground: Story = {
|
||||
args: {
|
||||
count: 3,
|
||||
icon: iconMap.bell,
|
||||
danger: false,
|
||||
soft: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
count: 42,
|
||||
icon: iconMap.bell,
|
||||
danger: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Soft: Story = {
|
||||
args: {
|
||||
count: 7,
|
||||
icon: iconMap.comments,
|
||||
soft: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Capped: Story = {
|
||||
args: {
|
||||
count: 150,
|
||||
icon: iconMap.bell,
|
||||
},
|
||||
}
|
||||
|
||||
export const Zero: Story = {
|
||||
args: {
|
||||
count: 0,
|
||||
icon: iconMap.bell,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
import { AxeBuilder } from '@axe-core/playwright'
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
const STORY_URL = '/iframe.html?id=ocelot-countericon'
|
||||
const STORY_ROOT = '#storybook-root'
|
||||
|
||||
async function waitForFonts(page: Page) {
|
||||
await page.evaluate(async () => document.fonts.ready)
|
||||
}
|
||||
|
||||
async function checkA11y(page: Page) {
|
||||
const results = await new AxeBuilder({ page }).include(STORY_ROOT).analyze()
|
||||
|
||||
expect(results.violations).toEqual([])
|
||||
}
|
||||
|
||||
test.describe('OsCounterIcon keyboard accessibility', () => {
|
||||
test('element is not focusable (decorative)', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--playground&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
|
||||
await page.keyboard.press('Tab')
|
||||
|
||||
// CounterIcon should not receive focus — it's decorative
|
||||
const counterIcon = root.locator('.os-counter-icon').first()
|
||||
const isFocused = await counterIcon.evaluate((el) => document.activeElement === el)
|
||||
|
||||
expect(isFocused).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('OsCounterIcon visual regression', () => {
|
||||
test('playground', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--playground&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
|
||||
await expect(root).toHaveScreenshot('playground.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('danger', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--danger&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
|
||||
await expect(root).toHaveScreenshot('danger.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('soft', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--soft&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
|
||||
await expect(root).toHaveScreenshot('soft.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('capped', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--capped&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
|
||||
await expect(root).toHaveScreenshot('capped.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
|
||||
test('zero', async ({ page }) => {
|
||||
await page.goto(`${STORY_URL}--zero&viewMode=story`)
|
||||
const root = page.locator(STORY_ROOT)
|
||||
await root.waitFor()
|
||||
await waitForFonts(page)
|
||||
|
||||
await expect(root).toHaveScreenshot('zero.png')
|
||||
|
||||
await checkA11y(page)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, h, isVue2 } from 'vue-demi'
|
||||
|
||||
import OsIcon from '#src/components/OsIcon/OsIcon.vue'
|
||||
|
||||
import type { Component, PropType } from 'vue-demi'
|
||||
|
||||
/**
|
||||
* Icon with a positioned counter badge.
|
||||
* Used for notification counts, comment counts, and similar indicators.
|
||||
* The badge appears only when count > 0 and caps at 99+.
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'OsCounterIcon',
|
||||
props: {
|
||||
/** Icon component or render function */
|
||||
icon: { type: [Object, Function] as PropType<Component>, required: true },
|
||||
/** Number to display in the badge */
|
||||
count: { type: Number, required: true },
|
||||
/** Use danger color for the badge */
|
||||
danger: { type: Boolean, default: false },
|
||||
/** Use soft/muted color for the badge */
|
||||
soft: { type: Boolean, default: false },
|
||||
},
|
||||
setup(props) {
|
||||
const cappedCount = computed(() => (props.count <= 99 ? String(props.count) : '99+'))
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
const classes = ['os-counter-icon__count']
|
||||
if (props.soft) classes.push('os-counter-icon__count--soft')
|
||||
else if (props.danger) classes.push('os-counter-icon__count--danger')
|
||||
return classes.join(' ')
|
||||
})
|
||||
|
||||
return () => {
|
||||
const icon = h(
|
||||
OsIcon,
|
||||
/* v8 ignore next -- Vue 2 */ isVue2
|
||||
? { props: { icon: props.icon } }
|
||||
: { icon: props.icon },
|
||||
)
|
||||
|
||||
const children = [icon]
|
||||
|
||||
if (props.count > 0) {
|
||||
children.push(
|
||||
h(
|
||||
'span',
|
||||
{ class: badgeClass.value },
|
||||
/* v8 ignore next -- Vue 2 */ isVue2 ? [cappedCount.value] : cappedCount.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return h('span', { class: 'os-counter-icon' }, children)
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.os-counter-icon {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.os-counter-icon__count {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
transform: translateX(50%);
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
padding: 3px;
|
||||
border-radius: 50%;
|
||||
|
||||
color: var(--os-counter-icon-color, var(--color-primary-contrast));
|
||||
background-color: var(--os-counter-icon-bg, var(--color-primary));
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.os-counter-icon__count--danger {
|
||||
background-color: var(--os-counter-icon-danger-bg, var(--color-danger));
|
||||
}
|
||||
|
||||
.os-counter-icon__count--soft {
|
||||
background-color: var(--os-counter-icon-soft-bg, var(--color-default));
|
||||
color: var(--os-counter-icon-soft-color, var(--color-text-soft));
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1012 B |
Binary file not shown.
|
After Width: | Height: | Size: 1016 B |
Binary file not shown.
|
After Width: | Height: | Size: 898 B |
Binary file not shown.
|
After Width: | Height: | Size: 878 B |
Binary file not shown.
|
After Width: | Height: | Size: 550 B |
1
packages/ui/src/ocelot/components/OsCounterIcon/index.ts
Normal file
1
packages/ui/src/ocelot/components/OsCounterIcon/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as OsCounterIcon } from './OsCounterIcon.vue'
|
||||
@ -3,4 +3,5 @@ export { ocelotIcons } from './icons'
|
||||
|
||||
// Ocelot composite components — built from Os* primitives
|
||||
export { OsActionButton } from './components/OsActionButton'
|
||||
export { OsCounterIcon } from './components/OsCounterIcon'
|
||||
export { OsLabeledButton } from './components/OsLabeledButton'
|
||||
|
||||
@ -13,23 +13,22 @@
|
||||
}"
|
||||
>
|
||||
<template #icon>
|
||||
<counter-icon :icon="icons.chatBubble" :count="unreadRoomCount" danger />
|
||||
<os-counter-icon :icon="icons.chatBubble" :count="unreadRoomCount" danger />
|
||||
</template>
|
||||
</os-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsButton } from '@ocelot-social/ui'
|
||||
import { OsCounterIcon, OsButton } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import { unreadRoomsQuery, roomCountUpdated } from '~/graphql/Rooms'
|
||||
|
||||
export default {
|
||||
name: 'ChatNotificationMenu',
|
||||
components: {
|
||||
OsCounterIcon,
|
||||
OsButton,
|
||||
CounterIcon,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
||||
@ -89,7 +89,7 @@ describe('CommentList.vue', () => {
|
||||
|
||||
it('displays a comments counter that ignores disabled and deleted comments', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.count').text()).toEqual('1')
|
||||
expect(wrapper.find('.os-counter-icon__count').text()).toEqual('1')
|
||||
})
|
||||
|
||||
describe('scrollToAnchor mixin', () => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="comments" class="comment-list">
|
||||
<h3 class="title">
|
||||
<counter-icon :icon="icons.comments" :count="commentsCount" />
|
||||
<os-counter-icon :icon="icons.comments" :count="commentsCount" />
|
||||
{{ $t('common.comment', null, 0) }}
|
||||
</h3>
|
||||
<div v-if="post.comments" class="comments">
|
||||
@ -19,15 +19,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { OsCounterIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import CommentCard from '~/components/CommentCard/CommentCard'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
components: {
|
||||
CounterIcon,
|
||||
OsCounterIcon,
|
||||
CommentCard,
|
||||
},
|
||||
props: {
|
||||
@ -73,7 +73,7 @@ export default {
|
||||
> .title {
|
||||
margin-top: 0;
|
||||
|
||||
> .counter-icon {
|
||||
> .os-counter-icon {
|
||||
margin-right: $space-small;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ describe('NotificationMenu.vue', () => {
|
||||
it('renders as link without counter', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes('notifications-menu')).toBe(true)
|
||||
expect(() => wrapper.get('.count')).toThrow()
|
||||
expect(wrapper.find('.os-counter-icon__count').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('no dropdown is rendered', () => {
|
||||
@ -74,7 +74,7 @@ describe('NotificationMenu.vue', () => {
|
||||
it('renders as link without counter', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes('notifications-menu')).toBe(true)
|
||||
expect(() => wrapper.get('.count')).toThrow()
|
||||
expect(wrapper.find('.os-counter-icon__count').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('no dropdown is rendered', () => {
|
||||
@ -161,12 +161,14 @@ describe('NotificationMenu.vue', () => {
|
||||
|
||||
it('displays the number of unread notifications', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.count').text()).toEqual('2')
|
||||
expect(wrapper.find('.os-counter-icon__count').text()).toEqual('2')
|
||||
})
|
||||
|
||||
it('renders the counter in red', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.count').classes()).toContain('--danger')
|
||||
expect(wrapper.find('.os-counter-icon__count').classes()).toContain(
|
||||
'os-counter-icon__count--danger',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
}"
|
||||
>
|
||||
<template #icon>
|
||||
<counter-icon :icon="icons.bell" :count="unreadNotificationsCount" danger />
|
||||
<os-counter-icon :icon="icons.bell" :count="unreadNotificationsCount" danger />
|
||||
</template>
|
||||
</os-button>
|
||||
<dropdown
|
||||
@ -38,7 +38,7 @@
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<template #icon>
|
||||
<counter-icon :icon="icons.bell" :count="unreadNotificationsCount" danger />
|
||||
<os-counter-icon :icon="icons.bell" :count="unreadNotificationsCount" danger />
|
||||
</template>
|
||||
</os-button>
|
||||
</template>
|
||||
@ -83,7 +83,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import unionBy from 'lodash/unionBy'
|
||||
import { OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { OsCounterIcon, OsButton, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import {
|
||||
notificationQuery,
|
||||
@ -91,7 +91,6 @@ import {
|
||||
notificationAdded,
|
||||
markAllAsReadMutation,
|
||||
} from '~/graphql/User'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import NotificationsTable from '../NotificationsTable/NotificationsTable.vue'
|
||||
|
||||
@ -99,7 +98,7 @@ export default {
|
||||
name: 'NotificationMenu',
|
||||
components: {
|
||||
NotificationsTable,
|
||||
CounterIcon,
|
||||
OsCounterIcon,
|
||||
Dropdown,
|
||||
OsButton,
|
||||
OsIcon,
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="categories-placeholder"></div>
|
||||
<counter-icon
|
||||
<os-counter-icon
|
||||
:icon="icons.heartO"
|
||||
:count="post.shoutedCount"
|
||||
v-tooltip="{
|
||||
@ -98,7 +98,7 @@
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
/>
|
||||
<counter-icon
|
||||
<os-counter-icon
|
||||
:icon="icons.comments"
|
||||
:count="post.commentsCount"
|
||||
v-tooltip="{
|
||||
@ -106,7 +106,7 @@
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
/>
|
||||
<counter-icon
|
||||
<os-counter-icon
|
||||
:icon="icons.handPointer"
|
||||
:count="post.clickedCount"
|
||||
v-tooltip="{
|
||||
@ -114,7 +114,7 @@
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
/>
|
||||
<counter-icon
|
||||
<os-counter-icon
|
||||
:icon="icons.eye"
|
||||
:count="post.viewedTeaserCount"
|
||||
v-tooltip="{
|
||||
@ -166,11 +166,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsCard, OsIcon } from '@ocelot-social/ui'
|
||||
import { OsCounterIcon, OsCard, OsIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import Category from '~/components/Category'
|
||||
import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
import DateTimeRange from '~/components/DateTimeRange/DateTimeRange'
|
||||
import HcRibbon from '~/components/Ribbon'
|
||||
import LocationTeaser from '~/components/LocationTeaser/LocationTeaser'
|
||||
@ -188,9 +187,9 @@ export default {
|
||||
components: {
|
||||
Category,
|
||||
ContentMenu,
|
||||
OsCounterIcon,
|
||||
OsCard,
|
||||
OsIcon,
|
||||
CounterIcon,
|
||||
DateTimeRange,
|
||||
HcRibbon,
|
||||
LocationTeaser,
|
||||
@ -413,7 +412,7 @@ export default {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .counter-icon {
|
||||
> .os-counter-icon {
|
||||
display: block;
|
||||
margin-right: $space-small;
|
||||
opacity: $opacity-disabled;
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CounterIcon from './CounterIcon'
|
||||
import { OsIcon } from '@ocelot-social/ui'
|
||||
import { ocelotIcons } from '@ocelot-social/ui/ocelot'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('CounterIcon.vue', () => {
|
||||
let propsData, wrapper, count
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(CounterIcon, { propsData, localVue })
|
||||
}
|
||||
|
||||
describe('given a valid icon and count below 100', () => {
|
||||
beforeEach(() => {
|
||||
propsData = { icon: ocelotIcons.comments, count: 42 }
|
||||
wrapper = Wrapper()
|
||||
count = wrapper.find('.count')
|
||||
})
|
||||
|
||||
it('renders the icon', () => {
|
||||
expect(wrapper.findComponent(OsIcon).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the count', () => {
|
||||
expect(count.text()).toEqual('42')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a valid icon and count above 100', () => {
|
||||
beforeEach(() => {
|
||||
propsData = { icon: ocelotIcons.comments, count: 750 }
|
||||
wrapper = Wrapper()
|
||||
count = wrapper.find('.count')
|
||||
})
|
||||
|
||||
it('renders the icon', () => {
|
||||
expect(wrapper.findComponent(OsIcon).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the capped count with a plus', () => {
|
||||
expect(count.text()).toEqual('99+')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,41 +0,0 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import CounterIcon from './CounterIcon.vue'
|
||||
|
||||
storiesOf('Generic/CounterIcon', module)
|
||||
.addDecorator(helpers.layout)
|
||||
|
||||
.add('default', () => ({
|
||||
components: { CounterIcon },
|
||||
template: `
|
||||
<counter-icon icon="flag" :count="3" />
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('high count', () => ({
|
||||
components: { CounterIcon },
|
||||
template: `
|
||||
<counter-icon icon="comments" :count="150" />
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('danger', () => ({
|
||||
components: { CounterIcon },
|
||||
template: `
|
||||
<counter-icon icon="bell" :count="42" danger />
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('soft', () => ({
|
||||
components: { CounterIcon },
|
||||
template: `
|
||||
<counter-icon icon="bell" :count="42" soft />
|
||||
`,
|
||||
}))
|
||||
|
||||
.add('count is 0', () => ({
|
||||
components: { CounterIcon },
|
||||
template: `
|
||||
<counter-icon icon="bell" :count="0" />
|
||||
`,
|
||||
}))
|
||||
@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<span class="counter-icon">
|
||||
<os-icon :icon="icon" />
|
||||
<span v-if="count > 0" :class="counterClass">{{ cappedCount }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsIcon } from '@ocelot-social/ui'
|
||||
|
||||
export default {
|
||||
components: { OsIcon },
|
||||
props: {
|
||||
icon: { type: [Object, Function], required: true },
|
||||
count: { type: Number, required: true },
|
||||
danger: { type: Boolean, default: false },
|
||||
soft: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
cappedCount() {
|
||||
return this.count <= 99 ? this.count : '99+'
|
||||
},
|
||||
counterClass() {
|
||||
let counterClass = 'count'
|
||||
if (this.soft) counterClass += ' --soft'
|
||||
else if (this.danger) counterClass += ' --danger'
|
||||
if (this.count === 0) counterClass += ' --inactive'
|
||||
|
||||
return counterClass
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.counter-icon {
|
||||
position: relative;
|
||||
|
||||
> .count {
|
||||
position: absolute;
|
||||
top: -$space-xx-small;
|
||||
right: 0;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: $size-icon-base;
|
||||
min-width: $size-icon-base;
|
||||
padding: 3px; // magic number to center count
|
||||
border-radius: 50%;
|
||||
transform: translateX(50%);
|
||||
|
||||
color: $color-neutral-100;
|
||||
background-color: $color-primary;
|
||||
font-size: 10px; // magic number to center count
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
|
||||
&.--danger {
|
||||
background-color: $color-danger;
|
||||
}
|
||||
|
||||
&.--inactive {
|
||||
background-color: $color-neutral-60;
|
||||
}
|
||||
|
||||
&.--soft {
|
||||
background-color: $color-neutral-90;
|
||||
color: $text-color-soft;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -29,10 +29,6 @@ export default {
|
||||
align-items: flex-end;
|
||||
color: $text-color-softer;
|
||||
font-size: $font-size-small;
|
||||
|
||||
> .counts > .counter-icon {
|
||||
margin: 0 $space-x-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
<p class="label">{{ option.title | truncate(70) }}</p>
|
||||
<div class="metadata">
|
||||
<span class="counts">
|
||||
<counter-icon :icon="icons.comments" :count="option.commentsCount" soft />
|
||||
<counter-icon :icon="icons.heartO" :count="option.shoutedCount" soft />
|
||||
<counter-icon :icon="icons.handPointer" :count="option.clickedCount" soft />
|
||||
<counter-icon :icon="icons.eye" :count="option.viewedTeaserCount" soft />
|
||||
<os-counter-icon :icon="icons.comments" :count="option.commentsCount" soft />
|
||||
<os-counter-icon :icon="icons.heartO" :count="option.shoutedCount" soft />
|
||||
<os-counter-icon :icon="icons.handPointer" :count="option.clickedCount" soft />
|
||||
<os-counter-icon :icon="icons.eye" :count="option.viewedTeaserCount" soft />
|
||||
</span>
|
||||
{{ option.author.name | truncate(32) }} - {{ option.createdAt | dateTime('dd.MM.yyyy') }}
|
||||
</div>
|
||||
@ -14,13 +14,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OsCounterIcon } from '@ocelot-social/ui'
|
||||
import { iconRegistry } from '~/utils/iconRegistry'
|
||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||
|
||||
export default {
|
||||
name: 'SearchPost',
|
||||
components: {
|
||||
CounterIcon,
|
||||
OsCounterIcon,
|
||||
},
|
||||
props: {
|
||||
option: { type: Object, required: true },
|
||||
@ -46,7 +46,7 @@ export default {
|
||||
color: $text-color-softer;
|
||||
font-size: $font-size-small;
|
||||
|
||||
> .counts > .counter-icon {
|
||||
> .counts > .os-counter-icon {
|
||||
margin: 0 $space-x-small;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user