Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions docs/app/components/content/ComponentProps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,6 @@ const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {
// @ts-expect-error - Type is not correct
prop.type = !prop.type.startsWith('boolean') && prop.schema?.kind === 'enum' && Object.keys(prop.schema.schema)?.length ? Object.values(prop.schema.schema).map(schema => schema?.type ? schema.type : schema).join(' | ') : prop.type
return prop
}).filter((prop) => {
/**
* @memo remove depricate props
* @see docs/server/utils/transformMDC.ts
*/
if (['depth', 'activeDepth'].includes(prop.name)) {
return false
}

return true
}).sort((a, b) => {
if (a.name === 'as') {
return -1
Expand Down Expand Up @@ -151,6 +141,7 @@ const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => {

<MDC v-if="prop.description" :value="prop.description" class="text-toned mt-1" :cache-key="`${kebabCase(route.path)}-${prop.name}-description`" />

<ComponentPropsDeprecated v-if="prop.tags?.length" :prop="prop" />
<ComponentPropsLinks v-if="prop.tags?.length" :prop="prop" />
<ComponentPropsSchema v-if="prop.schema" :prop="prop" :ignore="ignore" />
</ProseTd>
Expand Down
39 changes: 39 additions & 0 deletions docs/app/components/content/ComponentPropsDeprecated.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { kebabCase } from 'scule'
import type { PropertyMeta } from 'vue-component-meta'

const props = defineProps<{
prop: PropertyMeta
}>()

const route = useRoute()

/**
* Reads the `@deprecated` / `@removed` pair off the prop's JSDoc.
*
* Both survive the pipeline unfiltered: `vue-component-meta` passes every tag
* through, and `compactProp` drops only `defaultValue`. Before this component
* existed, nothing in the docs read `@deprecated` at all — the two props that
* carried a deprecation were hidden by a hardcoded name list instead, which
* left a third one (`Textarea`'s `fixed`) rendering as an ordinary prop while
* its JSDoc said it does nothing.
*/
const deprecated = computed(() => props.prop.tags?.find((tag: any) => tag.name === 'deprecated'))
const removed = computed(() => props.prop.tags?.find((tag: any) => tag.name === 'removed')?.text?.trim())
</script>

<template>
<div v-if="deprecated" class="mt-2">
<B24Badge
size="xs"
color="air-primary-warning"
:label="removed ? `Deprecated — removed in ${removed}` : 'Deprecated'"
/>
<MDC
v-if="deprecated.text"
:value="deprecated.text"
class="text-toned mt-1"
:cache-key="`${kebabCase(route.path)}-${prop.name}-deprecated`"
/>
</div>
</template>
24 changes: 16 additions & 8 deletions docs/server/utils/transformMDC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,6 @@ function propItemHandler(propValue: any): string {
// Props are pre-normalized by `compactProp`, so `type` is always a string
let propType: string = propValue.type

/**
* @memo remove depricate props
* @see docs/app/components/content/ComponentProps.vue
*/
if (['depth', 'activeDepth'].includes(propName)) {
return ''
}
/**
* @memo customize color property
* @todo test all colors
Expand Down Expand Up @@ -242,8 +235,13 @@ function propItemHandler(propValue: any): string {
const isRequired = propValue.required || false
const hasDescription = propValue.description && propValue.description.trim().length > 0
const hasDefault = propValue.default !== undefined
// Carried through rather than dropped: this output is what the `/raw/` pages
// and `llms.txt` serve, so a consumer reading it needs to know a prop is on
// its way out. `depth`/`activeDepth` used to be removed here by name.
const deprecated = propValue.tags?.find((tag: any) => tag.name === 'deprecated')
const removed = propValue.tags?.find((tag: any) => tag.name === 'removed')?.text?.trim()
let result = ''
if (hasDescription || hasDefault) {
if (hasDescription || hasDefault || deprecated) {
result += ` /**\n`
if (hasDescription) {
const descLines = propValue.description.split(/\r?\n/)
Expand All @@ -255,6 +253,16 @@ function propItemHandler(propValue: any): string {
const defaultValue = propValue.default
result += ` * @default ${typeof defaultValue === 'string' ? defaultValue : JSON.stringify(defaultValue)}\n`
}
if (deprecated) {
const deprecatedLines = (deprecated.text ?? '').split(/\r?\n/)
result += ` * @deprecated${deprecatedLines[0] ? ` ${deprecatedLines[0]}` : ''}\n`
deprecatedLines.slice(1).forEach((line: string) => {
result += ` * ${line.trim()}\n`
})
if (removed) {
result += ` * @removed ${removed}\n`
}
}
result += ` */\n`
}
result += ` ${propName}${isRequired ? '' : '?'}: ${propType};\n`
Expand Down
11 changes: 10 additions & 1 deletion src/runtime/components/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ export interface ButtonProps extends Omit<UseComponentIconsProps, 'trailing' | '
color?: Button['variants']['color']
activeColor?: Button['variants']['color']
/**
* @depricate
* @deprecated Renders nothing on the `air-*` colours, the default among
* them: the `depth` compound variants are keyed on the legacy colour
* names only.
*
* @removed 3.0.0
* @defaultValue 'normal'
*/
depth?: Button['variants']['depth']
/**
* @deprecated Same as `depth` — no effect on the `air-*` colours.
*
* @removed 3.0.0
*/
activeDepth?: Button['variants']['depth']
/**
* @defaultValue 'md'
Expand Down
9 changes: 8 additions & 1 deletion src/runtime/components/Textarea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@ export interface TextareaProps<T extends TextareaValue = TextareaValue, Mod exte
tagColor?: BadgeProps['color']
/** Highlight the ring color like a focus state. */
highlight?: boolean
/** Keep the mobile text size on all breakpoints. (Left for backward compatibility.) */
/**
* Keep the mobile text size on all breakpoints. (Left for backward compatibility.)
* @deprecated Does nothing. It works through `fixed` x `size` compound
* variants, and `Textarea` deliberately has no `size`, so no variant ever
* matches.
*
* @removed 3.0.0
*/
fixed?: boolean
defaultValue?: ApplyModifiers<T, Mod>
modelValue?: ApplyModifiers<T, Mod>
Expand Down
23 changes: 21 additions & 2 deletions test/components/Advice.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { describe } from 'vitest'
import { describe, it, expect } from 'vitest'
import { axe } from 'vitest-axe'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { renderEach } from '../component-render'
import Advice from '../../src/runtime/components/Advice.vue'
import Search2Icon from '@bitrix24/b24icons-vue/main/Search2Icon'

describe('Advice', () => {
renderEach(Advice, [
// Props
['with as', { props: { as: 'div' } }],
['with description', { props: { description: 'Description' } }],
['with angle', { props: { description: 'Description', angle: 'top' as const } }],
['with icon', { props: { description: 'Description', icon: Search2Icon } }],
['with avatar', { props: { description: 'Description', avatar: { alt: 'Bitrix24' } } }],
['with class', { props: { class: '' } }],
['with b24ui', { props: { b24ui: {} } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }]
['with default slot', { slots: { default: () => 'Default slot' } }],
['with leading slot', { slots: { leading: () => 'Leading slot' } }]
])

// The avatar rather than the icon: `icon` sets `isLeading`, which wins the
// `v-else-if` and drops the avatar, leaving axe one rule to run. The avatar
// renders an `<img>`, which brings in `image-alt` and `nested-interactive`.
it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(Advice, {
props: { description: 'Description', avatar: { src: 'https://github.com/bitrix24.png', alt: 'Bitrix24' } }
})

expect(await axe(wrapper.element)).toHaveNoViolations()
})
})
42 changes: 42 additions & 0 deletions test/components/Button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,54 @@ describe('Button', () => {
['with as', { props: { label: 'Button', as: 'div' } }],
['with class', { props: { label: 'Button', class: 'rounded-full font-(--ui-font-weight-bold)' } }],
['with b24ui', { props: { label: 'Button', b24ui: { label: 'font-bold' } } }],
// Bitrix24-only props, absent from `nuxt/ui`. `normalCase` defaults to
// `true`, so `false` is the case that renders something. `loadingAuto` is
// left out: it only reacts to an async click handler.
['with normalCase false', { props: { label: 'Button', normalCase: false } }],
['with loading and useWait', { props: { label: 'Button', loading: true, useWait: true } }],
['with loading and useClock', { props: { label: 'Button', loading: true, useClock: true } }],
['with inactiveClass', { props: { label: 'Button', inactiveClass: 'is-off' } }],
['with active and activeClass', { props: { label: 'Button', active: true, activeClass: 'is-on' } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],
['with leading slot', { slots: { leading: () => 'Leading slot' } }],
['with trailing slot', { slots: { trailing: () => 'Trailing slot' } }]
])

/**
* `activeColor` and `activeDepth` land on colours the snapshot matrix
* already renders, so a `renderEach` case for either one collides with a
* sibling and proves nothing (#454). What is actually worth pinning is the
* override: the prop must take effect only while the button is active.
*/
describe('active overrides', () => {
const render = async (props: Record<string, any>) =>
(await mountSuspended(Button, { props: { label: 'Button', ...props } })).html()

it('applies activeColor only while active', async () => {
const inactive = await render({ activeColor: 'air-primary-success' })
const active = await render({ active: true, activeColor: 'air-primary-success' })

expect(active).not.toBe(inactive)
expect(active).toBe(await render({ active: true, color: 'air-primary-success' }))
expect(inactive).toBe(await render({}))
})

// On a legacy colour name, because `depth` has compound variants only for
// those — on the `air-*` default the prop renders nothing either way, so
// the assertion would hold for the wrong reason. That gap is why both
// `depth` and `activeDepth` are `@deprecated`; the test stays, so the
// behaviour is pinned until they go in `3.0.0`.
it('applies activeDepth only while active', async () => {
const inactive = await render({ color: 'primary', activeDepth: 'dark' })
const active = await render({ color: 'primary', active: true, activeDepth: 'dark' })

expect(active).not.toBe(inactive)
expect(active).toBe(await render({ color: 'primary', active: true, depth: 'dark' }))
expect(inactive).toBe(await render({ color: 'primary' }))
})
})

test('with loading-auto works', async () => {
let resolve: any | null = null
const wrapper = await mountSuspended({
Expand Down
16 changes: 16 additions & 0 deletions test/components/Countdown.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { axe } from 'vitest-axe'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { defineComponent, h, ref, nextTick, KeepAlive } from 'vue'
import Countdown from '../../src/runtime/components/Countdown.vue'
Expand Down Expand Up @@ -29,6 +30,21 @@ describe('Countdown', () => {
['with leading slot', { slots: { leading: () => 'Leading slot' } }]
])

// The avatar is what makes this test a test. Ring plus digits alone matches
// no axe rule — the first version of this case reported zero rules run and
// was green by vacancy. The avatar renders an `<img>` and brings in
// `image-alt` / `nested-interactive`.
//
// `needStartImmediately: false` so the frame loop never starts: an axe run
// takes long enough that a live countdown would tick the DOM underneath it.
it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(Countdown, {
props: { seconds: 100, useCircle: true, needStartImmediately: false, avatar: { src: 'https://github.com/bitrix24.png', alt: 'Bitrix24' } }
})

expect(await axe(wrapper.element)).toHaveNoViolations()
})

describe('computed values', () => {
it('calculates time units correctly', async () => {
const wrapper = await mountSuspended(Countdown, {
Expand Down
13 changes: 13 additions & 0 deletions test/components/Input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ describe('Input', () => {
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'absolute' } }],
['with b24ui', { props: { b24ui: { base: 'rounded-full' } } }],
// Bitrix24-only props, absent from `nuxt/ui` — so nothing that arrives with
// an upstream sync covers them. `autofocus`/`autofocusDelay` are left out
// on purpose: they move focus and change no markup, so a snapshot pins
// nothing.
['with noPadding', { props: { noPadding: true } }],
['with noBorder', { props: { noBorder: true } }],
['with underline', { props: { underline: true } }],
['with rounded', { props: { rounded: true } }],
['with fixed', { props: { fixed: true } }],
['with autocomplete', { props: { autocomplete: 'email' } }],
['with tag', { props: { tag: 'Tag' } }],
['with tag and tagColor', { props: { tag: 'Tag', tagColor: 'air-primary-success' } }],
['with defaultValue', { props: { defaultValue: 'preset' } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],
['with leading slot', { slots: { leading: () => 'Leading slot' } }],
Expand Down
11 changes: 11 additions & 0 deletions test/components/InputNumber.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ describe('InputNumber', () => {
['with as', { props: { as: 'section' } }],
['with class', { props: { class: 'absolute' } }],
['with b24ui', { props: { b24ui: { base: 'rounded-full' } } }],
// Bitrix24-only props, absent from `nuxt/ui`. `autofocus`/`autofocusDelay`
// are left out: they move focus and change no markup.
['with noBorder', { props: { noBorder: true } }],
['with underline', { props: { underline: true } }],
['with rounded', { props: { rounded: true } }],
['with fixed', { props: { fixed: true } }],
['with tag', { props: { tag: 'Tag' } }],
['with tag and tagColor', { props: { tag: 'Tag', tagColor: 'air-primary-success' } }],
['with incrementDisabled', { props: { incrementDisabled: true } }],
['with decrementDisabled', { props: { decrementDisabled: true } }],
['with defaultValue', { props: { defaultValue: 7 } }],
// Slots
['with increment slot', { slots: { increment: () => '+' } }],
['with decrement slot', { slots: { decrement: () => '-' } }]
Expand Down
13 changes: 12 additions & 1 deletion test/components/ProsePrompt.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe } from 'vitest'
import { describe, it, expect } from 'vitest'
import { axe } from 'vitest-axe'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { renderEach } from '../component-render'
import ProsePrompt from '../../src/runtime/components/prose/Prompt.vue'
import Search2Icon from '@bitrix24/b24icons-vue/main/Search2Icon'
Expand All @@ -15,4 +17,13 @@ describe('ProsePrompt', () => {
// Slots
['with default slot', { slots: { default: () => 'Prompt body' } }]
])

it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(ProsePrompt, {
props: { description: 'Description', actions: ['copy', 'cursor', 'windsurf', 'claude'] as const },
slots: { default: () => 'Prompt body' }
})

expect(await axe(wrapper.element)).toHaveNoViolations()
})
})
9 changes: 9 additions & 0 deletions test/components/Select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ describe('Select', () => {
['with ariaLabel', { props, attrs: { 'aria-label': 'Aria label' } }],
['with class', { props: { ...props, class: 'rounded-full' } }],
['with b24ui', { props: { ...props, b24ui: { group: 'p-2' } } }],
// Bitrix24-only props, absent from `nuxt/ui`. `content` and `autofocus*`
// are left out: the first only applies to the open popover, the second two
// move focus without changing markup.
['with noPadding', { props: { ...props, noPadding: true } }],
['with noBorder', { props: { ...props, noBorder: true } }],
['with underline', { props: { ...props, underline: true } }],
['with rounded', { props: { ...props, rounded: true } }],
['with tag', { props: { ...props, tag: 'Tag' } }],
['with tag and tagColor', { props: { ...props, tag: 'Tag', tagColor: 'air-primary-success' } }],
// Slots
['with leading slot', { props, slots: { leading: () => 'Leading slot' } }],
['with trailing slot', { props, slots: { trailing: () => 'Trailing slot' } }],
Expand Down
24 changes: 23 additions & 1 deletion test/components/TableWrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe } from 'vitest'
import { describe, it, expect } from 'vitest'
import { h } from 'vue'
import { axe } from 'vitest-axe'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import TableWrapper from '../../src/runtime/components/TableWrapper.vue'
import { renderEach } from '../component-render'

Expand All @@ -21,4 +24,23 @@ describe('TableWrapper', () => {
// Slots
['with default slot', { slots: { default: () => '<table><tbody><tr><th>1</th><td>2</td></tr></tbody></table>' } }]
])

// Built with `h` rather than handed over as a string. A string slot is
// escaped to text, so the table never reaches the DOM and axe runs *zero*
// rules — the first version of this case was green by vacancy, and even
// went red under a `role="table"` mutation for the wrong reason (no rows to
// find, because there was no table). As vnodes it runs the table rules.
it('passes accessibility tests', async () => {
const wrapper = await mountSuspended(TableWrapper, {
slots: {
default: () => h('table', [
h('caption', 'Deals'),
h('thead', [h('tr', [h('th', { scope: 'col' }, 'Name'), h('th', { scope: 'col' }, 'Sum')])]),
h('tbody', [h('tr', [h('th', { scope: 'row' }, 'First'), h('td', '100')])])
])
}
})

expect(await axe(wrapper.element)).toHaveNoViolations()
})
})
12 changes: 12 additions & 0 deletions test/components/Textarea.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ describe('Textarea', () => {
['with class', { props: { class: 'w-48' } }],
// @memo wrapper not exist at theme ////
['with b24ui', { props: { b24ui: { root: 'ms-4' } } }],
// Bitrix24-only props, absent from `nuxt/ui`. Left out on purpose:
// `autofocus`/`autofocusDelay` only move focus; `maxrows`/`autoresizeDelay`
// need real layout, which happy-dom does not do; and `fixed` is inert and
// now `@deprecated` on the component. Not pinned here: a snapshot of a
// no-op would read as certifying it.
['with noPadding', { props: { noPadding: true } }],
['with noBorder', { props: { noBorder: true } }],
['with underline', { props: { underline: true } }],
['with rounded', { props: { rounded: true } }],
['with tag', { props: { tag: 'Tag' } }],
['with tag and tagColor', { props: { tag: 'Tag', tagColor: 'air-primary-success' } }],
['with defaultValue', { props: { defaultValue: 'preset' } }],
// Slots
['with default slot', { slots: { default: () => 'Default slot' } }],
['with leading slot', { slots: { leading: () => 'Leading slot' } }],
Expand Down
Loading
Loading