Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript Rollout Tier 5 - Toast #349

Merged
merged 7 commits into from
Jan 6, 2025
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
1 change: 0 additions & 1 deletion packages/buefy-next/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ const JS_COMPONENTS = [
'tag',
'taginput',
'timepicker',
'toast',
'upload',
]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { transformVNodeArgs } from 'vue'
import { shallowMount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { BToast, ToastProgrammatic } from '@components/toast'

let wrapper
let wrapper: VueWrapper<InstanceType<typeof BToast>>

describe('BToast', () => {
HTMLElement.prototype.insertAdjacentElement = jest.fn()
HTMLElement.prototype.insertAdjacentElement = vi.fn()
beforeEach(() => {
wrapper = shallowMount(
BToast,
Expand Down Expand Up @@ -38,32 +40,32 @@ describe('BToast', () => {
})

afterEach(() => {
jest.useRealTimers()
vi.useRealTimers()
})

it('should close after the duration', () => {
jest.useFakeTimers()
vi.useFakeTimers()
const params = {
message: 'message',
duration: 1000,
onClose: jest.fn()
onClose: vi.fn()
}
new ToastProgrammatic().open(params)
jest.advanceTimersByTime(500)
vi.advanceTimersByTime(500)
expect(params.onClose).not.toHaveBeenCalled()
jest.advanceTimersByTime(500)
vi.advanceTimersByTime(500)
expect(params.onClose).toHaveBeenCalled()
})

it('indefinitely should be able to be manually closed', () => {
jest.useFakeTimers()
vi.useFakeTimers()
const params = {
message: 'message',
indefinite: true,
onClose: jest.fn()
onClose: vi.fn()
}
const toast = new ToastProgrammatic().open(params)
jest.advanceTimersByTime(10000)
vi.advanceTimersByTime(10000)
expect(params.onClose).not.toHaveBeenCalled()
toast.close()
expect(params.onClose).toHaveBeenCalled()
Expand Down
13 changes: 10 additions & 3 deletions packages/buefy-next/src/components/toast/Toast.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,24 @@
</transition>
</template>

<script>
<script lang="ts">
import { defineComponent } from 'vue'

import config from '../../utils/config'
import type { ExtractComponentProps } from '../../utils/helpers'
import NoticeMixin from '../../utils/NoticeMixin'

export default {
const Toast = defineComponent({
name: 'BToast',
mixins: [NoticeMixin],
data() {
return {
newDuration: this.duration || config.defaultToastDuration
}
}
}
})

export type ToastProps = ExtractComponentProps<typeof Toast>

export default Toast
</script>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`BToast > render correctly 1`] = `
"<transition-stub enteractiveclass=\\"fadeInDown\\" leaveactiveclass=\\"fadeOut\\" appear=\\"false\\" persisted=\\"true\\" css=\\"true\\">
<div class=\\"toast is-dark is-top\\" aria-hidden=\\"false\\" role=\\"alert\\" style=\\"\\">
<!-- eslint-disable-next-line vue/no-v-html -->
<div></div>
</div>
</transition-stub>"
`;
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
import { createApp, h as createElement } from 'vue'
import type { App, ComponentPublicInstance, VNode } from 'vue'

import Toast from './Toast.vue'
import type { ToastProps } from './Toast.vue'

import config from '../../utils/config'
import { merge, copyAppContext, getComponentFromVNode } from '../../utils/helpers'
import { copyAppContext, getComponentFromVNode } from '../../utils/helpers'
import { registerComponentProgrammatic } from '../../utils/plugins'

export type ToastOpenParams = Omit<ToastProps, 'message'> & {
// programmatically opened toast can have VNode(s) as the message
message?: string | VNode | (string | VNode)[],
onClose?: () => void
}

// Minimal definition of a programmatically opened toast.
//
// ESLint does not like `{}` as a type but allowed here to make them look
// similar to Vue's definition.
/* eslint-disable @typescript-eslint/ban-types */
type ToastProgrammaticInstance = ComponentPublicInstance<
{}, // P
{}, // B
{}, // D
{}, // C
{ close: () => void } // M
>
/* eslint-enable @typescript-eslint/ban-types */

class ToastProgrammatic {
constructor(app) {
private app: App | undefined

constructor(app?: App) {
this.app = app // may be undefined in the testing environment
}

open(params) {
open(params: string | ToastOpenParams) {
if (typeof params === 'string') {
params = {
message: params
}
}

const defaultParam = {
position: config.defaultToastPosition || 'is-top'
}
if (params.parent) {
delete params.parent
let slot: ToastOpenParams['message']
let { message, ...restParams } = params
if (typeof message !== 'string') {
slot = message
message = undefined
}
let slot
if (Array.isArray(params.message)) {
slot = params.message
delete params.message
const propsData: ToastProps = {
position: config.defaultToastPosition || 'is-top',
message,
...restParams
}
const propsData = merge(defaultParam, params)
const container = document.createElement('div')
// Vue 3 requires a new app to mount another component
const vueInstance = createApp({
Expand All @@ -42,7 +65,7 @@ class ToastProgrammatic {
close() {
const toast = getComponentFromVNode(this.toastVNode)
if (toast) {
toast.close()
(toast as InstanceType<typeof Toast>).close()
}
}
},
Expand Down Expand Up @@ -76,14 +99,15 @@ class ToastProgrammatic {
} else {
// adds $buefy global property
// so that $buefy.globalNoticeInterval is available on the new Vue app
vueInstance.config.globalProperties.$buefy = {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vueInstance.config.globalProperties.$buefy = {} as any
}
return vueInstance.mount(container)
return vueInstance.mount(container) as ToastProgrammaticInstance
}
}

const Plugin = {
install(Vue) {
install(Vue: App) {
registerComponentProgrammatic(Vue, 'toast', new ToastProgrammatic(Vue))
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/buefy-next/src/utils/vue-augmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'vue'
import type { LoadingProgrammatic } from '../components/loading'
import type { ModalProgrammatic } from '../components/modal'
import type { SnackbarProgrammatic } from '../components/snackbar'
import type { ToastProgrammatic } from '../components/toast'
import ConfigComponent from './ConfigComponent'

// Augments the global property with `$buefy`.
Expand All @@ -24,6 +25,7 @@ declare module '@vue/runtime-core' {
loading: LoadingProgrammatic,
modal: ModalProgrammatic,
snackbar: SnackbarProgrammatic,
toast: ToastProgrammatic,
// TODO: make key-values more specific
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
Expand Down
30 changes: 21 additions & 9 deletions packages/docs/src/pages/components/toast/Toast.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,30 @@
</div>
</template>

<script>
import { preformat } from '@/utils'
<script lang="ts">
import { defineComponent } from 'vue'

import { preformat, shallowFields } from '@/utils'
import ApiView from '@/components/ApiView.vue'
import CodeView from '@/components/CodeView.vue'
import Example from '@/components/Example.vue'
import VariablesView from '@/components/VariablesView.vue'

import api from './api/toast'
import variables from './variables/toast'
import { shallowFields } from '@/utils'

import ExSimple from './examples/ExSimple'
import ExSimple from './examples/ExSimple.vue'
import ExSimpleCode from './examples/ExSimple.vue?raw'

export default {
import outsideVueInstance from './outside-vue-instance.js?raw'

export default defineComponent({
components: {
ApiView,
CodeView,
Example,
VariablesView
},
data() {
return {
api,
Expand All @@ -35,13 +49,11 @@
ExSimple
}),
ExSimpleCode,
outsideVueInstance: `
import { ToastProgrammatic as Toast } from 'buefy'
Toast.open('Toasty!')`
outsideVueInstance,
}
},
methods: {
preformat
}
}
})
</script>
15 changes: 11 additions & 4 deletions packages/docs/src/pages/components/toast/examples/ExSimple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@
</section>
</template>

<script>
export default {
<script lang="ts">
import { defineComponent } from 'vue'
import { BButton } from '@ntohq/buefy-next'
import type { ToastProgrammatic } from '@ntohq/buefy-next'

type ToastProgrammaticInstance = ReturnType<ToastProgrammatic['open']>

export default defineComponent({
components: { BButton },
data() {
return {
indefinteToast: null
indefinteToast: null as ToastProgrammaticInstance | null
}
},
methods: {
Expand Down Expand Up @@ -85,5 +92,5 @@
}
}
}
}
})
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { ToastProgrammatic as Toast } from 'buefy'
Toast.open('Toasty!')
Loading