Skip to content

feat(Modal/Slideover): add close method in slots #4219

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

Merged
merged 3 commits into from
May 28, 2025

Conversation

JosephAnson
Copy link
Contributor

@JosephAnson JosephAnson commented May 25, 2025

πŸ”— Linked issue

❓ Type of change

  • πŸ“– Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • πŸ‘Œ Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

πŸ“š Description

Implemented programmatic close method for Modal component scoped slots.

New Feature:

  • Programmatic close method - Modal now exposes a close method through scoped slots, allowing modals to be closed from within their content without requiring external ref management.
  • Scoped slot implementation - Most scoped slots now provide access to the close method #body="{ close }", #footer="{ close }", etc.

Documentation Updates:

  • Added ModalProgramaticClose.vue example demonstrating the new programmatic close functionality.
  • Shows how to eliminate the need for manual v-model:open ref management.
  • Demonstrates usage in both body and footer slots.

Benefits:

  • No need to create and manage boolean refs for modal state
  • Reduces boilerplate code and cognitive load
  • Modal handles its own internal state management

Old ref management way

<script setup lang="ts">
const first = ref(false)
const second = ref(false)
</script>

<template>
  <UModal v-model:open="first" title="First modal" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <UModal v-model:open="second" title="Second modal" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </UModal>
    </template>
  </UModal>
</template>

New slot close method

<template>
  <UModal title="First modal" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

    <template #footer={ close: closeA }>
      <UButton label="Close" color="neutral" variant="outline" @click="closeA" />

      <UModal title="Second modal" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

        <template #footer={ close: closeB }>
          <UButton label="Close" color="neutral" variant="outline" @click="closeB" />
        </template>
      </UModal>
    </template>
  </UModal>
</template>

πŸ“ Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Considerations

The only way to expose the Dialog functionality was to create a new ModalContext.vue and expose the injected code via a slot, however maybe editing the Reka UI Dialog component to expose their methods on the DialogRoot might make things less complicated.

<script lang="ts">
import type { Ref } from 'vue'
import { createContext } from '@/shared'

export interface DialogRootProps {
  /** The controlled open state of the dialog. Can be binded as `v-model:open`. */
  open?: boolean
  /** The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. */
  defaultOpen?: boolean
  /**
   * The modality of the dialog When set to `true`, <br>
   * interaction with outside elements will be disabled and only dialog content will be visible to screen readers.
   */
  modal?: boolean
}

export type DialogRootEmits = {
  /** Event handler called when the open state of the dialog changes. */
  'update:open': [value: boolean]
}

export interface DialogRootContext {
  open: Readonly<Ref<boolean>>
  modal: Ref<boolean>
  openModal: () => void
  onOpenChange: (value: boolean) => void
  onOpenToggle: () => void
  triggerElement: Ref<HTMLElement | undefined>
  contentElement: Ref<HTMLElement | undefined>
  contentId: string
  titleId: string
  descriptionId: string
}

export const [injectDialogRootContext, provideDialogRootContext]
  = createContext<DialogRootContext>('DialogRoot')
</script>

<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { ref, toRefs } from 'vue'

defineOptions({
  inheritAttrs: false,
})

const props = withDefaults(defineProps<DialogRootProps>(), {
  open: undefined,
  defaultOpen: false,
  modal: true,
})
const emit = defineEmits<DialogRootEmits>()

defineSlots<{
  default: (props: {
    /** Current open state */
    open: typeof open.value
    /** Close the dialog */
    close: () => void
  }) => any
}>()

const open = useVModel(props, 'open', emit, {
  defaultValue: props.defaultOpen,
  passive: (props.open === undefined) as false,
}) as Ref<boolean>

const triggerElement = ref<HTMLElement>()
const contentElement = ref<HTMLElement>()
const { modal } = toRefs(props)

provideDialogRootContext({
  open,
  modal,
  openModal: () => {
    open.value = true
  },
  onOpenChange: (value) => {
    open.value = value
  },
  onOpenToggle: () => {
    open.value = !open.value
  },
  contentId: '',
  titleId: '',
  descriptionId: '',
  triggerElement,
  contentElement,
})

function close() {
  open.value = false
}
</script>

<template>
  <slot
    :open="open"
    :close="close"
  />
</template>

@JosephAnson JosephAnson force-pushed the feat/modal-scoped-slots branch from 5591d5d to 9d2fb6f Compare May 25, 2025 19:23
Copy link

pkg-pr-new bot commented May 25, 2025

npm i https://pkg.pr.new/@nuxt/ui@4219

commit: d8daafe

Copy link
Member

@benjamincanac benjamincanac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice addition but it would even be better if the Reka UI DialogRoot component could expose this directly πŸ€”

@JosephAnson
Copy link
Contributor Author

Yeah I thought so too, having it from the DialogRoot would be ideal. I've got the change for Reka UI needed in this PR, so I can create the PR there. I'll update this PR later with the changes if I'm able to get it merged into Reka.

Copy link
Member

Feel free to link the PR here once ready so I can have a look. Having it in DialogRoot would benefit the Modal, Slideover and Drawer components, this could be useful to implement this in the Popover as well.

@JosephAnson JosephAnson force-pushed the feat/modal-scoped-slots branch from 4a7cac3 to 6b6dadf Compare May 26, 2025 09:22
@JosephAnson
Copy link
Contributor Author

JosephAnson commented May 26, 2025

I've worked on the current implementation it a little bit more this morning and I think my approach now is cleaner for doing the closing internally with NuxtUI by introducing internal state for open using useVModel.

I'll still create the PR for DialogRoot as it does make it cleaner without adding a local useVModel to each component.

@JosephAnson JosephAnson force-pushed the feat/modal-scoped-slots branch 2 times, most recently from 6d78674 to 4d3082b Compare May 27, 2025 17:54
@JosephAnson JosephAnson changed the title feat(Modal): implement programmatic close method for slots feat(modal,slideover): implement programmatic close method for slots May 27, 2025
@JosephAnson JosephAnson changed the title feat(modal,slideover): implement programmatic close method for slots feat(Modal,Slideover): implement programmatic close method for slots May 27, 2025
@JosephAnson
Copy link
Contributor Author

JosephAnson commented May 27, 2025

Hey @benjamincanac, the updates to Reka UI have been completed and i've also updated the slideover to support the same design in this PR, for the Drawer vaul-vue will need a version bump. There are conflicts with the new version of Reka UI and the Calendar / Textarea, the version will need to be bumped and the conflict resolved before this can be merged.

@benjamincanac
Copy link
Member

@JosephAnson There are quite a few changes necessary to upgrade to [email protected] but it's ongoing here: #4234

@benjamincanac benjamincanac changed the title feat(Modal,Slideover): implement programmatic close method for slots feat(Modal/Slideover): proxy close method in slots May 28, 2025
@benjamincanac
Copy link
Member

This branch now has [email protected], would you mind reverting the pnpm-lock.yaml though? git checkout origin/v3 pnpm-lock.yaml

@JosephAnson JosephAnson force-pushed the feat/modal-scoped-slots branch from 120f6d7 to fbf2e77 Compare May 28, 2025 12:50
@JosephAnson JosephAnson force-pushed the feat/modal-scoped-slots branch from fbf2e77 to 9b2a0cd Compare May 28, 2025 13:04
Copy link
Member

@benjamincanac benjamincanac left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up removing the examples to demonstrate this inside the With footer slot!

@benjamincanac benjamincanac changed the title feat(Modal/Slideover): proxy close method in slots feat(Modal/Slideover): add close method in slots May 28, 2025
@benjamincanac
Copy link
Member

benjamincanac commented May 28, 2025

As a note, we'll need to do this in the Drawer component as well (which requires an update on vaul-vue) and on the Popover component (which requires an update to reka-ui popover component).

@JosephAnson
Copy link
Contributor Author

I can make a PR for reka popover if you'd like? I've already made the PR for vaul-vue here: unovue/vaul-vue#106

@benjamincanac benjamincanac merged commit 5835eb5 into nuxt:v3 May 28, 2025
6 checks passed
@JosephAnson JosephAnson deleted the feat/modal-scoped-slots branch May 28, 2025 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants