diff --git a/design-library/src/components/BccDialog/BccDialog.css b/design-library/src/components/BccDialog/BccDialog.css new file mode 100644 index 00000000..a5df9d91 --- /dev/null +++ b/design-library/src/components/BccDialog/BccDialog.css @@ -0,0 +1,83 @@ + +@layer components { + .bcc-dialog { + @apply relative flex flex-col gap-6 rounded-lg bg-primary p-6 text-primary shadow-xl sm:min-w-[28.5rem] sm:max-w-[28.5rem] xl:max-w-[32rem] max-h-[80vh]; + } + + /* Overlay/Wrapper */ + .bcc-dialog-overlay-wrapper { + @apply fixed inset-0 z-50; + } + + .bcc-dialog-container { + @apply flex h-[100dvh] w-[100dvw] items-center justify-center overflow-hidden; + } + + .bcc-dialog-overlay { + @apply fixed inset-0 bg-black/80 backdrop-blur-sm transition-opacity; + } + + .bcc-dialog-wrapper { + @apply flex h-full w-full transform flex-col justify-end transition-all sm:items-center sm:justify-center p-4; + } + + /* Header */ + .bcc-dialog-header + .bcc-dialog-body { + @apply border-t border-on-primary pt-5; + } + + /* Body */ + .bcc-dialog-body { + @apply flex flex-col gap-2 overflow-y-auto; + } + + /* Title */ + .bcc-dialog-title { + @apply text-heading-lg flex justify-between items-start; + } + .bcc-dialog-close-button { + @apply text-black; + } + .bcc-dialog-close-icon { + @apply h-6 w-6; + } + + .bcc-dialog-subtitle { + @apply text-caption-lg text-secondary; + } + + /* Content */ + .bcc-dialog-content { + @apply text-body overflow-y-auto py-6; + } + + /* Footer */ + .bcc-dialog-footer { + @apply sticky bottom-0 left-0 right-0 bg-primary; + } + + .bcc-dialog-footer-content { + @apply mb-5; + } + + /* Actions */ + .bcc-dialog-actions { + @apply flex gap-4; + } + .bcc-dialog-secondary-action { + @apply flex-1 w-full; + button { + @apply w-full; + } + } + .bcc-dialog-primary-action { + @apply flex-1 w-full; + button { + @apply w-full; + } + } + + .bcc-dialog-alert .bcc-dialog-actions { + @apply justify-center; + } +} diff --git a/design-library/src/components/BccDialog/BccDialog.spec.ts b/design-library/src/components/BccDialog/BccDialog.spec.ts new file mode 100644 index 00000000..ded08921 --- /dev/null +++ b/design-library/src/components/BccDialog/BccDialog.spec.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from "vitest"; +import { mount } from "@vue/test-utils"; +import BccDialog from "./BccDialog.vue"; // replace with the actual component name +import BccButton from "../BccButton/BccButton.vue"; + +describe("BccDialog", () => { + it("renders a dialog", () => { + const wrapper = mount(BccDialog, { + props: { open: true, title: "Dialog title", subtitle: "Dialog subtitle" }, + slots: { + default: "Dialog content", + header: "Header slot", + footer: "Footer slot", + primaryAction: "Primary action", + secondaryAction: "Secondary action", + }, + global: { + renderStubDefaultSlot: true, + }, + shallow: true, + }); + + expect(wrapper.text()).toContain("Header slot"); + expect(wrapper.text()).toContain("Dialog title"); + expect(wrapper.text()).toContain("Dialog subtitle"); + expect(wrapper.text()).toContain("Dialog content"); + expect(wrapper.text()).toContain("Footer slot"); + expect(wrapper.text()).toContain("Primary action"); + expect(wrapper.text()).toContain("Secondary action"); + }); + + it("closes when the close button is clicked", async () => { + const wrapper = mount(BccDialog, { + props: { open: true }, + slots: { + default: "Dialog content", + }, + global: { + renderStubDefaultSlot: true, + }, + shallow: true, + }); + + await wrapper.find(".bcc-dialog-close-button").trigger("click"); + + expect(wrapper.emitted("close")?.length).toBe(1); + }); + + it("doesn't show the close button when there is a header", () => { + const wrapper = mount(BccDialog, { + props: { open: true }, + slots: { + default: "Dialog content", + header: "Header content", + }, + global: { + renderStubDefaultSlot: true, + }, + shallow: true, + }); + + expect(wrapper.html()).not.toContain("bcc-dialog-close-button"); + }); + + it("renders the correct variant class", () => { + const wrapper = mount(BccDialog, { + props: { open: true, variant: "alert" }, + slots: { + default: "Dialog content", + }, + global: { + renderStubDefaultSlot: true, + }, + shallow: true, + }); + + expect(wrapper.find(".bcc-dialog").classes()).toContain("bcc-dialog-alert"); + }); + + it("renders the correct button context for primary action", () => { + const wrapper = mount(BccDialog, { + props: { open: true, destructive: true }, + slots: { + primaryAction: "Primary action", + }, + global: { + components: { + BccButton, + }, + renderStubDefaultSlot: true, + }, + shallow: true, + }); + + const primaryButton = wrapper.findComponent(BccButton); + expect(primaryButton.exists()).toBe(true); + expect(primaryButton.props("context")).toBe("danger"); + }); +}); diff --git a/design-library/src/components/BccDialog/BccDialog.stories.ts b/design-library/src/components/BccDialog/BccDialog.stories.ts new file mode 100644 index 00000000..506f8f67 --- /dev/null +++ b/design-library/src/components/BccDialog/BccDialog.stories.ts @@ -0,0 +1,132 @@ +import BccButton from "../BccButton/BccButton.vue"; +import BccDialog from "./BccDialog.vue"; + +import type { Meta, StoryFn } from "@storybook/vue3"; + +export default { + title: "Common/BccDialog", + component: BccDialog, + argTypes: { + variant: { + control: { type: "select" }, + options: ["action", "alert"], + }, + destructive: { + control: { type: "boolean" }, + }, + }, +} as Meta; + +const Template: StoryFn = (args) => ({ + components: { BccDialog, BccButton }, + setup() { + return { args }; + }, + template: ` +
+ + + + + + + + + Open dialog +
+ `, +}); + +// Default story +export const Default = Template.bind({}); +Default.args = { + open: false, + title: "Question?", + subtitle: "Subtitle if needed", + closeButton: true, + variant: "action", + destructive: false, + slotDefault: "", + slotSecondaryAction: true, + slotPrimaryAction: true, +}; +Default.parameters = { + docs: { + source: { + language: "html", + code: ` + + Do you want to update something? + + + + + + `, + }, + }, +}; + +export const ActionVariant = Template.bind({}); +ActionVariant.args = { + ...Default.args, + variant: "action", + destructive: false, +}; + +export const DestructiveAction = Template.bind({}); +DestructiveAction.args = { + ...Default.args, + variant: "action", + destructive: true, +}; + +export const DestructiveAlert = Template.bind({}); +DestructiveAlert.args = { + ...Default.args, + variant: "alert", + destructive: true, // Enforce destructive for alert +}; + +export const NoActions = Template.bind({}); +NoActions.args = { + open: false, + title: "Question?", + closeButton: true, + variant: "action", + destructive: false, + slotDefault: "", + slotSecondaryAction: false, + slotPrimaryAction: false, +}; +NoActions.parameters = { + docs: { + source: { + language: "html", + code: ` + + Do you want to update something? + + `, + }, + }, +}; diff --git a/design-library/src/components/BccDialog/BccDialog.vue b/design-library/src/components/BccDialog/BccDialog.vue new file mode 100644 index 00000000..a987e157 --- /dev/null +++ b/design-library/src/components/BccDialog/BccDialog.vue @@ -0,0 +1,131 @@ + + + diff --git a/design-library/src/css/index.css b/design-library/src/css/index.css index 429d60eb..3326f699 100644 --- a/design-library/src/css/index.css +++ b/design-library/src/css/index.css @@ -34,3 +34,4 @@ @import "../components/BccTooltip/BccTooltip.css"; @import "../components/BccAccordion/BccAccordion.css"; @import "../components/BccPagination/BccPagination.css"; +@import "../components/BccDialog/BccDialog.css"; diff --git a/design-library/src/index.ts b/design-library/src/index.ts index a7d22e6d..e9bf2260 100644 --- a/design-library/src/index.ts +++ b/design-library/src/index.ts @@ -41,3 +41,4 @@ export { default as BccStepper } from "./components/BccStepper/BccStepper.vue"; export { default as BccTooltip } from "./components/BccTooltip/BccTooltip.vue"; export { default as BccAccordion } from "./components/BccAccordion/BccAccordion.vue"; export { default as BccPagination } from "./components/BccPagination/BccPagination.vue"; +export { default as BccDialog } from "./components/BccDialog/BccDialog.vue";