From 36fdfec88c39ddef2253af961363ed408ecf9d4c Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 2 May 2023 22:30:52 +0200 Subject: [PATCH 001/154] feat(dialog): add dialog --- .../src/components/dialog/dialog.spec.tsx | 24 +++++ .../src/components/dialog/dialog.tsx | 96 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 packages/kit-headless/src/components/dialog/dialog.spec.tsx create mode 100644 packages/kit-headless/src/components/dialog/dialog.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.spec.tsx b/packages/kit-headless/src/components/dialog/dialog.spec.tsx new file mode 100644 index 000000000..89f1666c4 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.spec.tsx @@ -0,0 +1,24 @@ +import { mount } from 'cypress-ct-qwik'; +import * as Dialog from './dialog'; + +describe('Dialog', () => { + it('renders an opened Dialog', () => { + mount( + +

Hello World!

+
+ ); + + cy.get('dialog').should('contain', 'Hello World'); + }); + + it('does not show if Dialog is closed', () => { + mount( + +

Hello World!

+
+ ); + + cy.get('dialog').should('not.be.visible'); + }); +}); diff --git a/packages/kit-headless/src/components/dialog/dialog.tsx b/packages/kit-headless/src/components/dialog/dialog.tsx new file mode 100644 index 000000000..52b6d597c --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.tsx @@ -0,0 +1,96 @@ +import { + $, + QRL, + QwikMouseEvent, + Slot, + component$, + createContextId, + useComputed$, + useContextProvider, + useSignal, + useStore, + useVisibleTask$, +} from '@builder.io/qwik'; + +export type DialogState = { + opened: boolean; +}; + +export type DialogContext = { + state: DialogState; + open: QRL<() => void>; + close: QRL<() => void>; +}; + +export type RootProps = { + open: boolean; + type?: 'modal' | 'bottom-sheet' | 'side-nav'; +}; + +export const dialogContext = createContextId('dialog'); + +export const Root = component$((props: RootProps) => { + const dialogRef = useSignal(); + const classes = useComputed$(() => [props.type ?? 'modal']); + + const state = useStore({ + opened: false, + }); + + const openDialog$ = $(() => { + const dialog = dialogRef.value; + + if (!dialog) { + throw new Error( + '[Qwik UI Dialog]: Cannot open the Dialog. -Element not found.' + ); + } + + dialog.showModal(); + state.opened = true; + }); + + const closeDialog$ = $(() => { + const dialog = dialogRef.value; + + if (!dialog) { + throw new Error( + '[Qwik UI Dialog]: Cannot close the Dialog. -Element not found.' + ); + } + + dialog.close(); + state.opened = false; + }); + + const handleClick$ = $( + ( + event: QwikMouseEvent, + element: HTMLDialogElement + ) => { + if (event.target !== element) return; + + return closeDialog$(); + } + ); + + const context: DialogContext = { + state, + open: openDialog$, + close: closeDialog$, + }; + + useContextProvider(dialogContext, context); + + useVisibleTask$(async ({ track }) => { + const shallBeOpened = track(() => props.open); + + shallBeOpened ? await openDialog$() : await closeDialog$(); + }); + + return ( + + + + ); +}); From bfc48a7f376fbba25b5849c0ae4bd0bf920284fc Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 2 May 2023 23:03:44 +0200 Subject: [PATCH 002/154] feat(dialog): introduce Dialog.Trigger & Dialog.Portal --- .../src/components/dialog/dialog.tsx | 67 +++++++++++++------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.tsx b/packages/kit-headless/src/components/dialog/dialog.tsx index 52b6d597c..37cd7375f 100644 --- a/packages/kit-headless/src/components/dialog/dialog.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.tsx @@ -2,43 +2,46 @@ import { $, QRL, QwikMouseEvent, + Signal, Slot, component$, createContextId, useComputed$, + useContext, useContextProvider, + useOn, useSignal, useStore, - useVisibleTask$, } from '@builder.io/qwik'; export type DialogState = { opened: boolean; + dialogRef: Signal; }; export type DialogContext = { state: DialogState; + open: QRL<() => void>; close: QRL<() => void>; -}; - -export type RootProps = { - open: boolean; - type?: 'modal' | 'bottom-sheet' | 'side-nav'; + closeOnDialogClick: QRL< + ( + event: QwikMouseEvent, + element: HTMLDialogElement + ) => void + >; }; export const dialogContext = createContextId('dialog'); -export const Root = component$((props: RootProps) => { - const dialogRef = useSignal(); - const classes = useComputed$(() => [props.type ?? 'modal']); - +export const Root = component$(() => { const state = useStore({ opened: false, + dialogRef: useSignal(), }); const openDialog$ = $(() => { - const dialog = dialogRef.value; + const dialog = state.dialogRef.value; if (!dialog) { throw new Error( @@ -51,7 +54,7 @@ export const Root = component$((props: RootProps) => { }); const closeDialog$ = $(() => { - const dialog = dialogRef.value; + const dialog = state.dialogRef.value; if (!dialog) { throw new Error( @@ -63,7 +66,7 @@ export const Root = component$((props: RootProps) => { state.opened = false; }); - const handleClick$ = $( + const closeOnDialogClick$ = $( ( event: QwikMouseEvent, element: HTMLDialogElement @@ -76,21 +79,47 @@ export const Root = component$((props: RootProps) => { const context: DialogContext = { state, + open: openDialog$, close: closeDialog$, + closeOnDialogClick: closeOnDialogClick$, }; useContextProvider(dialogContext, context); - useVisibleTask$(async ({ track }) => { - const shallBeOpened = track(() => props.open); + return ; +}); - shallBeOpened ? await openDialog$() : await closeDialog$(); - }); +export const Trigger = component$(() => { + const context = useContext(dialogContext); + + useOn( + 'click', + $(() => context.open()) + ); + + return ( +
+ +
+ ); +}); + +export type PortalProps = { + type?: 'modal' | 'bottom-sheet' | 'side-nav'; +}; + +export const Portal = component$((props: PortalProps) => { + const context = useContext(dialogContext); + const classes = useComputed$(() => [props.type ?? 'modal']); return ( - - + + ); }); From b2b4a4d6acee49096627c976124dfaec5102bdde Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 2 May 2023 23:03:57 +0200 Subject: [PATCH 003/154] test(dialog): provide basic tests --- .../src/components/dialog/dialog.spec.tsx | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.spec.tsx b/packages/kit-headless/src/components/dialog/dialog.spec.tsx index 89f1666c4..45a8eaa9d 100644 --- a/packages/kit-headless/src/components/dialog/dialog.spec.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.spec.tsx @@ -4,18 +4,51 @@ import * as Dialog from './dialog'; describe('Dialog', () => { it('renders an opened Dialog', () => { mount( - -

Hello World!

+ + + + + +

Hello World!

+
); - cy.get('dialog').should('contain', 'Hello World'); + cy.get('button').contains(/open/i).click(); + + cy.get('[data-test=dialog-title]') + .should('be.visible') + .should('contain', 'Hello World'); + }); + + it('closes on backdrop-click', () => { + mount( + + + + + +

Hello World!

+
+
+ ); + + cy.get('button').contains(/open/i).click(); + + cy.get('dialog').click('top'); + + cy.get('dialog').should('not.be.visible'); }); it('does not show if Dialog is closed', () => { mount( - -

Hello World!

+ + + + + +

Hello World!

+
); From 64250a8a00632fdb8c0d69fd8c1abdec1720f2c3 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 2 May 2023 23:09:11 +0200 Subject: [PATCH 004/154] refactor(dialog): remove class-overrides --- .../kit-headless/src/components/dialog/dialog.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.tsx b/packages/kit-headless/src/components/dialog/dialog.tsx index 37cd7375f..eb135bdaa 100644 --- a/packages/kit-headless/src/components/dialog/dialog.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.tsx @@ -6,7 +6,6 @@ import { Slot, component$, createContextId, - useComputed$, useContext, useContextProvider, useOn, @@ -105,20 +104,11 @@ export const Trigger = component$(() => { ); }); -export type PortalProps = { - type?: 'modal' | 'bottom-sheet' | 'side-nav'; -}; - -export const Portal = component$((props: PortalProps) => { +export const Portal = component$(() => { const context = useContext(dialogContext); - const classes = useComputed$(() => [props.type ?? 'modal']); return ( - + ); From cc26112c5a6c56f7d78a36bb88308cfe9b86ff2d Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 4 May 2023 17:03:42 +0200 Subject: [PATCH 005/154] docs(headless): add dialog --- .../headless/(components)/dialog/examples.tsx | 146 ++++++++++++++++++ .../headless/(components)/dialog/index.mdx | 43 ++++++ packages/kit-headless/src/index.ts | 1 + 3 files changed, 190 insertions(+) create mode 100644 apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx create mode 100644 apps/website/src/routes/docs/headless/(components)/dialog/index.mdx diff --git a/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx new file mode 100644 index 000000000..6c06dcd95 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx @@ -0,0 +1,146 @@ +import { component$, Slot } from '@builder.io/qwik'; +import { Accordion, AccordionItem, Checkbox, Dialog } from '@qwik-ui/headless'; +import { PreviewCodeExample } from '../../../../../components/preview-code-example/preview-code-example'; + +export const Example01 = component$(() => { + return ( + +
+ + + + + Hallo Welt + +
+ +
+ +
+
+ ); +}); + +export const Example02 = component$(() => { + return ( + +
+ + +

+ Yes, Qwik just hit a major milestone and launched v1.0! All API + features are considered stable. Start building the future, today! +

+
+ +

+ You're looking at one right now! +

+
+ +

+ We're glad you asked. Come join us at the Qwikifiers Discord + server or find the{` `} + + Qwik UI repository + + {` `} + on GitHub! +

+
+
+
+ +
+ +
+
+ ); +}); + +export const Example03 = component$(() => { + return ( + +
+ + +
    +
  • + + + In stock + +
  • +
  • + + + Out of stock + +
  • +
  • + + + Coming soon + +
  • +
+
+ +
    +
  • + + + 50% off on selected products + +
  • +
  • + + + Winter specials + +
  • +
+
+ +
    +
  • + + + Books + +
  • +
  • + + + Stationery + +
  • +
  • + + + Storage + +
  • +
+
+
+
+ +
+ +
+
+ ); +}); diff --git a/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx b/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx new file mode 100644 index 000000000..36a259651 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx @@ -0,0 +1,43 @@ +--- +title: Qwik UI | Dialog +--- + +import { Dialog } from '@qwik-ui/headless'; +import { Example01 } from './examples'; +import { KeyboardInteractionTable } from '../../../../../components/keyboard-interaction-table/keyboard-interaction-table'; + +# Dialog + +#### A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. ([Definition comes from Radix-UI](https://www.radix-ui.com/docs/primitives/components/dialog)) + +{' '} + + + ```tsx + + + + + +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus + aliquid architecto delectus deleniti dolor +

+
+ +
+ ``` +
+ +## Accessibility + +### Keyboard interaction + + diff --git a/packages/kit-headless/src/index.ts b/packages/kit-headless/src/index.ts index 4c593ebb7..06cb3b7d3 100644 --- a/packages/kit-headless/src/index.ts +++ b/packages/kit-headless/src/index.ts @@ -8,6 +8,7 @@ export * as Carousel from './components/carousel/carousel'; export * from './components/carousel/use'; export * from './components/pagination/pagination'; export * from './components/collapse/collapse'; +export * as Dialog from './components/dialog/dialog' export * from './components/drawer'; export * from './components/input-phone'; export * as Input from './components/input/input'; From ea5ba0e5cc356ddb0800987e421a713261f42894 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 12 May 2023 13:01:51 +0200 Subject: [PATCH 006/154] fix(dialog): close dialog on backdrop click --- .../headless/(components)/dialog/examples.tsx | 2 +- .../headless/(components)/dialog/index.mdx | 3 +- .../tailwind/(components)/dialog/examples.tsx | 157 ++++++++++++++++++ .../tailwind/(components)/dialog/index.mdx | 42 +++++ .../src/components/dialog/dialog.spec.tsx | 3 + .../src/components/dialog/dialog.tsx | 46 ++++- .../src/components/dialog/dialog.tsx | 16 ++ packages/kit-tailwind/src/index.ts | 19 ++- 8 files changed, 267 insertions(+), 21 deletions(-) create mode 100644 apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx create mode 100644 apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx create mode 100644 packages/kit-tailwind/src/components/dialog/dialog.tsx diff --git a/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx index 6c06dcd95..f2f183e52 100644 --- a/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx @@ -10,7 +10,7 @@ export const Example01 = component$(() => { - Hallo Welt + Hello World
diff --git a/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx b/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx index 36a259651..2933234ed 100644 --- a/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx @@ -20,8 +20,7 @@ import { KeyboardInteractionTable } from '../../../../../components/keyboard-int

- Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus - aliquid architecto delectus deleniti dolor + Hello World

diff --git a/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx new file mode 100644 index 000000000..f065738c9 --- /dev/null +++ b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx @@ -0,0 +1,157 @@ +import { component$, Slot } from '@builder.io/qwik'; +import { + Accordion, + AccordionItem, + Button, + Checkbox, + Dialog, +} from '@qwik-ui/tailwind'; +import { PreviewCodeExample } from '../../../../../components/preview-code-example/preview-code-example'; + +export const Example01 = component$(() => { + return ( + +
+ + + + + + Hello World + + + + + +
+ +
+ +
+
+ ); +}); + +export const Example02 = component$(() => { + return ( + +
+ + +

+ Yes, Qwik just hit a major milestone and launched v1.0! All API + features are considered stable. Start building the future, today! +

+
+ +

+ You're looking at one right now! +

+
+ +

+ We're glad you asked. Come join us at the Qwikifiers Discord + server or find the{` `} + + Qwik UI repository + + {` `} + on GitHub! +

+
+
+
+ +
+ +
+
+ ); +}); + +export const Example03 = component$(() => { + return ( + +
+ + +
    +
  • + + + In stock + +
  • +
  • + + + Out of stock + +
  • +
  • + + + Coming soon + +
  • +
+
+ +
    +
  • + + + 50% off on selected products + +
  • +
  • + + + Winter specials + +
  • +
+
+ +
    +
  • + + + Books + +
  • +
  • + + + Stationery + +
  • +
  • + + + Storage + +
  • +
+
+
+
+ +
+ +
+
+ ); +}); diff --git a/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx b/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx new file mode 100644 index 000000000..7a1a38844 --- /dev/null +++ b/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx @@ -0,0 +1,42 @@ +--- +title: Qwik UI | Dialog +--- + +import { Dialog, Button } from '@qwik-ui/tailwind'; +import { Example01 } from './examples'; +import { KeyboardInteractionTable } from '../../../../../components/keyboard-interaction-table/keyboard-interaction-table'; + +# Dialog + +#### A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. ([Definition comes from Radix-UI](https://www.radix-ui.com/docs/primitives/components/dialog)) + +{' '} + + + ```tsx + + + + + +

Hello World

+ + + +
+
+ ``` +
+ +## Accessibility + +### Keyboard interaction + + diff --git a/packages/kit-headless/src/components/dialog/dialog.spec.tsx b/packages/kit-headless/src/components/dialog/dialog.spec.tsx index 45a8eaa9d..b5f5f81ca 100644 --- a/packages/kit-headless/src/components/dialog/dialog.spec.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.spec.tsx @@ -9,6 +9,9 @@ describe('Dialog', () => { + {/* Dialog.Header */} + {/* Dialog.Content */} + {/* Dialog.Actions/Footer */}

Hello World!

diff --git a/packages/kit-headless/src/components/dialog/dialog.tsx b/packages/kit-headless/src/components/dialog/dialog.tsx index eb135bdaa..11eced4f4 100644 --- a/packages/kit-headless/src/components/dialog/dialog.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.tsx @@ -1,6 +1,7 @@ import { $, QRL, + QwikIntrinsicElements, QwikMouseEvent, Signal, Slot, @@ -66,13 +67,19 @@ export const Root = component$(() => { }); const closeOnDialogClick$ = $( - ( - event: QwikMouseEvent, - element: HTMLDialogElement - ) => { - if (event.target !== element) return; - - return closeDialog$(); + (event: QwikMouseEvent) => { + const rect = (event.target as HTMLDialogElement).getBoundingClientRect(); + + if ( + rect.left > event.clientX || + rect.right < event.clientX || + rect.top > event.clientY || + rect.bottom < event.clientY + ) { + return closeDialog$(); + } + + return Promise.resolve(); } ); @@ -104,11 +111,32 @@ export const Trigger = component$(() => { ); }); -export const Portal = component$(() => { +export const Close = component$(() => { + const context = useContext(dialogContext); + + useOn( + 'click', + $(() => context.close()) + ); + + return ( +
+ +
+ ); +}); + +type PortalProps = QwikIntrinsicElements['dialog']; + +export const Portal = component$((props: PortalProps) => { const context = useContext(dialogContext); return ( - + ); diff --git a/packages/kit-tailwind/src/components/dialog/dialog.tsx b/packages/kit-tailwind/src/components/dialog/dialog.tsx new file mode 100644 index 000000000..7cabdeb45 --- /dev/null +++ b/packages/kit-tailwind/src/components/dialog/dialog.tsx @@ -0,0 +1,16 @@ +import { Slot, component$ } from '@builder.io/qwik'; +import { Dialog } from '@qwik-ui/headless'; + +export const Root = Dialog.Root; + +export const Trigger = Dialog.Trigger; + +export const Close = Dialog.Close; + +export const Portal = component$(() => { + return ( + + + + ); +}); diff --git a/packages/kit-tailwind/src/index.ts b/packages/kit-tailwind/src/index.ts index 78b7e91e1..e7127cc0a 100644 --- a/packages/kit-tailwind/src/index.ts +++ b/packages/kit-tailwind/src/index.ts @@ -1,22 +1,23 @@ export * from './components/accordion/accordion'; export * from './components/alert/alert'; export * from './components/badge/badge'; -export * from './components/button/button'; -export * from './components/progress/progress'; +export * from './components/breadcrumb'; export * from './components/button-group/button-group'; +export * from './components/button/button'; export * from './components/card'; +export * from './components/checkbox/checkbox'; export * from './components/collapse/collapse'; +export * as Dialog from './components/dialog/dialog'; export * from './components/drawer/drawer'; -export * from './components/spinner/spinner'; +export * from './components/navigation-bar/navigation-bar'; +export * from './components/pagination/pagination'; export * from './components/popover/popover'; +export * from './components/progress/progress'; export * from './components/rating/rating'; +export * from './components/ratio/radio'; +export * from './components/slider/slider'; +export * from './components/spinner/spinner'; export * from './components/tabs'; export * from './components/toast/toast'; export * from './components/toggle/toggle'; export * from './components/tooltip/tooltip'; -export * from './components/checkbox/checkbox'; -export * from './components/pagination/pagination'; -export * from './components/ratio/radio'; -export * from './components/slider/slider'; -export * from './components/breadcrumb'; -export * from './components/navigation-bar/navigation-bar'; From bcd283b7806884e28e8e361b1fcf0594d91d5430 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 12 May 2023 13:03:30 +0200 Subject: [PATCH 007/154] refactor(dialog): clean up code --- .../src/components/dialog/dialog.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.tsx b/packages/kit-headless/src/components/dialog/dialog.tsx index 11eced4f4..e17537811 100644 --- a/packages/kit-headless/src/components/dialog/dialog.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.tsx @@ -67,20 +67,8 @@ export const Root = component$(() => { }); const closeOnDialogClick$ = $( - (event: QwikMouseEvent) => { - const rect = (event.target as HTMLDialogElement).getBoundingClientRect(); - - if ( - rect.left > event.clientX || - rect.right < event.clientX || - rect.top > event.clientY || - rect.bottom < event.clientY - ) { - return closeDialog$(); - } - - return Promise.resolve(); - } + (event: QwikMouseEvent) => + hasBackdropBeenClicked(event) ? closeDialog$() : Promise.resolve() ); const context: DialogContext = { @@ -141,3 +129,16 @@ export const Portal = component$((props: PortalProps) => { ); }); + +function hasBackdropBeenClicked( + event: QwikMouseEvent +) { + const rect = (event.target as HTMLDialogElement).getBoundingClientRect(); + + return ( + rect.left > event.clientX || + rect.right < event.clientX || + rect.top > event.clientY || + rect.bottom < event.clientY + ); +} From 964f34d2f4ae70d72cf4e439c74464821e013486 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 12 May 2023 13:25:35 +0200 Subject: [PATCH 008/154] refactor(dialog): split components in seperate files --- .../src/components/dialog/dialog.spec.tsx | 2 +- .../src/components/dialog/dialog.tsx | 144 ------------------ 2 files changed, 1 insertion(+), 145 deletions(-) delete mode 100644 packages/kit-headless/src/components/dialog/dialog.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.spec.tsx b/packages/kit-headless/src/components/dialog/dialog.spec.tsx index b5f5f81ca..a0bb31a6e 100644 --- a/packages/kit-headless/src/components/dialog/dialog.spec.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.spec.tsx @@ -1,5 +1,5 @@ import { mount } from 'cypress-ct-qwik'; -import * as Dialog from './dialog'; +import * as Dialog from './public_api'; describe('Dialog', () => { it('renders an opened Dialog', () => { diff --git a/packages/kit-headless/src/components/dialog/dialog.tsx b/packages/kit-headless/src/components/dialog/dialog.tsx deleted file mode 100644 index e17537811..000000000 --- a/packages/kit-headless/src/components/dialog/dialog.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { - $, - QRL, - QwikIntrinsicElements, - QwikMouseEvent, - Signal, - Slot, - component$, - createContextId, - useContext, - useContextProvider, - useOn, - useSignal, - useStore, -} from '@builder.io/qwik'; - -export type DialogState = { - opened: boolean; - dialogRef: Signal; -}; - -export type DialogContext = { - state: DialogState; - - open: QRL<() => void>; - close: QRL<() => void>; - closeOnDialogClick: QRL< - ( - event: QwikMouseEvent, - element: HTMLDialogElement - ) => void - >; -}; - -export const dialogContext = createContextId('dialog'); - -export const Root = component$(() => { - const state = useStore({ - opened: false, - dialogRef: useSignal(), - }); - - const openDialog$ = $(() => { - const dialog = state.dialogRef.value; - - if (!dialog) { - throw new Error( - '[Qwik UI Dialog]: Cannot open the Dialog. -Element not found.' - ); - } - - dialog.showModal(); - state.opened = true; - }); - - const closeDialog$ = $(() => { - const dialog = state.dialogRef.value; - - if (!dialog) { - throw new Error( - '[Qwik UI Dialog]: Cannot close the Dialog. -Element not found.' - ); - } - - dialog.close(); - state.opened = false; - }); - - const closeOnDialogClick$ = $( - (event: QwikMouseEvent) => - hasBackdropBeenClicked(event) ? closeDialog$() : Promise.resolve() - ); - - const context: DialogContext = { - state, - - open: openDialog$, - close: closeDialog$, - closeOnDialogClick: closeOnDialogClick$, - }; - - useContextProvider(dialogContext, context); - - return ; -}); - -export const Trigger = component$(() => { - const context = useContext(dialogContext); - - useOn( - 'click', - $(() => context.open()) - ); - - return ( -
- -
- ); -}); - -export const Close = component$(() => { - const context = useContext(dialogContext); - - useOn( - 'click', - $(() => context.close()) - ); - - return ( -
- -
- ); -}); - -type PortalProps = QwikIntrinsicElements['dialog']; - -export const Portal = component$((props: PortalProps) => { - const context = useContext(dialogContext); - - return ( - - - - ); -}); - -function hasBackdropBeenClicked( - event: QwikMouseEvent -) { - const rect = (event.target as HTMLDialogElement).getBoundingClientRect(); - - return ( - rect.left > event.clientX || - rect.right < event.clientX || - rect.top > event.clientY || - rect.bottom < event.clientY - ); -} From f6f40efe8d3e72c8a37310c55ad48a1d886e6ff4 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 12 May 2023 13:25:35 +0200 Subject: [PATCH 009/154] refactor(dialog): split components in separate files --- .../src/components/dialog/dialog.close.tsx | 17 +++++ .../src/components/dialog/dialog.context.tsx | 4 + .../src/components/dialog/dialog.portal.tsx | 23 ++++++ .../src/components/dialog/dialog.root.tsx | 74 +++++++++++++++++++ .../src/components/dialog/dialog.trigger.tsx | 17 +++++ .../src/components/dialog/public_api.ts | 5 ++ .../components/dialog/types/dialog-context.ts | 15 ++++ .../components/dialog/types/dialog-state.ts | 6 ++ .../src/components/dialog/types/index.ts | 2 + 9 files changed, 163 insertions(+) create mode 100644 packages/kit-headless/src/components/dialog/dialog.close.tsx create mode 100644 packages/kit-headless/src/components/dialog/dialog.context.tsx create mode 100644 packages/kit-headless/src/components/dialog/dialog.portal.tsx create mode 100644 packages/kit-headless/src/components/dialog/dialog.root.tsx create mode 100644 packages/kit-headless/src/components/dialog/dialog.trigger.tsx create mode 100644 packages/kit-headless/src/components/dialog/public_api.ts create mode 100644 packages/kit-headless/src/components/dialog/types/dialog-context.ts create mode 100644 packages/kit-headless/src/components/dialog/types/dialog-state.ts create mode 100644 packages/kit-headless/src/components/dialog/types/index.ts diff --git a/packages/kit-headless/src/components/dialog/dialog.close.tsx b/packages/kit-headless/src/components/dialog/dialog.close.tsx new file mode 100644 index 000000000..f369b6b19 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.close.tsx @@ -0,0 +1,17 @@ +import { $, Slot, component$, useContext, useOn } from '@builder.io/qwik'; +import { dialogContext } from './dialog.context'; + +export const Close = component$(() => { + const context = useContext(dialogContext); + + useOn( + 'click', + $(() => context.close()) + ); + + return ( +
+ +
+ ); +}); diff --git a/packages/kit-headless/src/components/dialog/dialog.context.tsx b/packages/kit-headless/src/components/dialog/dialog.context.tsx new file mode 100644 index 000000000..eaddd9262 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.context.tsx @@ -0,0 +1,4 @@ +import { createContextId } from '@builder.io/qwik'; +import { DialogContext } from './types'; + +export const dialogContext = createContextId('dialog'); diff --git a/packages/kit-headless/src/components/dialog/dialog.portal.tsx b/packages/kit-headless/src/components/dialog/dialog.portal.tsx new file mode 100644 index 000000000..aaaf24ce6 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.portal.tsx @@ -0,0 +1,23 @@ +import { + QwikIntrinsicElements, + Slot, + component$, + useContext, +} from '@builder.io/qwik'; +import { dialogContext } from './dialog.context'; + +type PortalProps = QwikIntrinsicElements['dialog']; + +export const Portal = component$((props: PortalProps) => { + const context = useContext(dialogContext); + + return ( + + + + ); +}); diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx new file mode 100644 index 000000000..236514252 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -0,0 +1,74 @@ +import { + $, + QwikMouseEvent, + Slot, + component$, + useContextProvider, + useSignal, + useStore, +} from '@builder.io/qwik'; +import { dialogContext } from './dialog.context'; +import { DialogContext } from './types'; + +export const Root = component$(() => { + const state = useStore({ + opened: false, + dialogRef: useSignal(), + }); + + const openDialog$ = $(() => { + const dialog = state.dialogRef.value; + + if (!dialog) { + throw new Error( + '[Qwik UI Dialog]: Cannot open the Dialog. -Element not found.' + ); + } + + dialog.showModal(); + state.opened = true; + }); + + const closeDialog$ = $(() => { + const dialog = state.dialogRef.value; + + if (!dialog) { + throw new Error( + '[Qwik UI Dialog]: Cannot close the Dialog. -Element not found.' + ); + } + + dialog.close(); + state.opened = false; + }); + + const closeOnDialogClick$ = $( + (event: QwikMouseEvent) => + hasBackdropBeenClicked(event) ? closeDialog$() : Promise.resolve() + ); + + const context: DialogContext = { + state, + + open: openDialog$, + close: closeDialog$, + closeOnDialogClick: closeOnDialogClick$, + }; + + useContextProvider(dialogContext, context); + + return ; +}); + +function hasBackdropBeenClicked( + event: QwikMouseEvent +) { + const rect = (event.target as HTMLDialogElement).getBoundingClientRect(); + + return ( + rect.left > event.clientX || + rect.right < event.clientX || + rect.top > event.clientY || + rect.bottom < event.clientY + ); +} diff --git a/packages/kit-headless/src/components/dialog/dialog.trigger.tsx b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx new file mode 100644 index 000000000..326b7b673 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx @@ -0,0 +1,17 @@ +import { $, Slot, component$, useContext, useOn } from '@builder.io/qwik'; +import { dialogContext } from './dialog.context'; + +export const Trigger = component$(() => { + const context = useContext(dialogContext); + + useOn( + 'click', + $(() => context.open()) + ); + + return ( +
+ +
+ ); +}); diff --git a/packages/kit-headless/src/components/dialog/public_api.ts b/packages/kit-headless/src/components/dialog/public_api.ts new file mode 100644 index 000000000..6e2c73a3c --- /dev/null +++ b/packages/kit-headless/src/components/dialog/public_api.ts @@ -0,0 +1,5 @@ +export * from './dialog.close'; +export * from './dialog.context'; +export * from './dialog.portal'; +export * from './dialog.root'; +export * from './dialog.trigger'; diff --git a/packages/kit-headless/src/components/dialog/types/dialog-context.ts b/packages/kit-headless/src/components/dialog/types/dialog-context.ts new file mode 100644 index 000000000..91816feb4 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/types/dialog-context.ts @@ -0,0 +1,15 @@ +import { QRL, QwikMouseEvent } from '@builder.io/qwik'; +import { DialogState } from './dialog-state'; + +export type DialogContext = { + state: DialogState; + + open: QRL<() => void>; + close: QRL<() => void>; + closeOnDialogClick: QRL< + ( + event: QwikMouseEvent, + element: HTMLDialogElement + ) => void + >; +}; diff --git a/packages/kit-headless/src/components/dialog/types/dialog-state.ts b/packages/kit-headless/src/components/dialog/types/dialog-state.ts new file mode 100644 index 000000000..e1b088371 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/types/dialog-state.ts @@ -0,0 +1,6 @@ +import { Signal } from '@builder.io/qwik'; + +export type DialogState = { + opened: boolean; + dialogRef: Signal; +}; diff --git a/packages/kit-headless/src/components/dialog/types/index.ts b/packages/kit-headless/src/components/dialog/types/index.ts new file mode 100644 index 000000000..98be73e3c --- /dev/null +++ b/packages/kit-headless/src/components/dialog/types/index.ts @@ -0,0 +1,2 @@ +export * from './dialog-context'; +export * from './dialog-state'; From 65d7b9a41596869946b6bb708caed97a6e4e2625 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 12 May 2023 15:28:58 +0200 Subject: [PATCH 010/154] docs(dialog): remove unused examples --- .../headless/(components)/dialog/examples.tsx | 126 +---------------- .../tailwind/(components)/dialog/examples.tsx | 132 +----------------- 2 files changed, 2 insertions(+), 256 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx index f2f183e52..6662514dd 100644 --- a/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx @@ -1,5 +1,5 @@ import { component$, Slot } from '@builder.io/qwik'; -import { Accordion, AccordionItem, Checkbox, Dialog } from '@qwik-ui/headless'; +import { Dialog } from '@qwik-ui/headless'; import { PreviewCodeExample } from '../../../../../components/preview-code-example/preview-code-example'; export const Example01 = component$(() => { @@ -20,127 +20,3 @@ export const Example01 = component$(() => { ); }); - -export const Example02 = component$(() => { - return ( - -
- - -

- Yes, Qwik just hit a major milestone and launched v1.0! All API - features are considered stable. Start building the future, today! -

-
- -

- You're looking at one right now! -

-
- -

- We're glad you asked. Come join us at the Qwikifiers Discord - server or find the{` `} - - Qwik UI repository - - {` `} - on GitHub! -

-
-
-
- -
- -
-
- ); -}); - -export const Example03 = component$(() => { - return ( - -
- - -
    -
  • - - - In stock - -
  • -
  • - - - Out of stock - -
  • -
  • - - - Coming soon - -
  • -
-
- -
    -
  • - - - 50% off on selected products - -
  • -
  • - - - Winter specials - -
  • -
-
- -
    -
  • - - - Books - -
  • -
  • - - - Stationery - -
  • -
  • - - - Storage - -
  • -
-
-
-
- -
- -
-
- ); -}); diff --git a/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx index f065738c9..294e72274 100644 --- a/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx +++ b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx @@ -1,11 +1,5 @@ import { component$, Slot } from '@builder.io/qwik'; -import { - Accordion, - AccordionItem, - Button, - Checkbox, - Dialog, -} from '@qwik-ui/tailwind'; +import { Button, Dialog } from '@qwik-ui/tailwind'; import { PreviewCodeExample } from '../../../../../components/preview-code-example/preview-code-example'; export const Example01 = component$(() => { @@ -31,127 +25,3 @@ export const Example01 = component$(() => { ); }); - -export const Example02 = component$(() => { - return ( - -
- - -

- Yes, Qwik just hit a major milestone and launched v1.0! All API - features are considered stable. Start building the future, today! -

-
- -

- You're looking at one right now! -

-
- -

- We're glad you asked. Come join us at the Qwikifiers Discord - server or find the{` `} - - Qwik UI repository - - {` `} - on GitHub! -

-
-
-
- -
- -
-
- ); -}); - -export const Example03 = component$(() => { - return ( - -
- - -
    -
  • - - - In stock - -
  • -
  • - - - Out of stock - -
  • -
  • - - - Coming soon - -
  • -
-
- -
    -
  • - - - 50% off on selected products - -
  • -
  • - - - Winter specials - -
  • -
-
- -
    -
  • - - - Books - -
  • -
  • - - - Stationery - -
  • -
  • - - - Storage - -
  • -
-
-
-
- -
- -
-
- ); -}); From c846303348f48912fa9cd939f05f8a02320c0a3b Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 18 May 2023 21:34:37 +0200 Subject: [PATCH 011/154] test(dialog): add storybook story --- .../src/components/dialog/dialog.stories.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/kit-headless/src/components/dialog/dialog.stories.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx new file mode 100644 index 000000000..7ce03bae9 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -0,0 +1,48 @@ +import { expect } from '@storybook/jest'; +import { userEvent, within } from '@storybook/testing-library'; +import { Meta, StoryObj } from 'storybook-framework-qwik'; +import * as Dialog from './public_api'; + +const meta: Meta = { + title: 'Dialog', + component: Dialog.Root, +}; + +type Story = StoryObj; + +export default meta; + +export const Primary: Story = { + args: { + dialogTrigger: { + text: 'Open Dialog', + }, + dialogPortal: { + text: 'Hello World', + }, + dialogClose: { + text: 'Close', + }, + }, + render: (args) => ( + <> + + + + + + {args.dialogPortal.text} + + + + + + + ), + play: ({ canvasElement, args }) => { + const canvas = within(canvasElement); + userEvent.click(canvas.getByText(args.dialogTrigger.text)); + expect(canvas.getByText(args.dialogPortal.text)).toBeTruthy(); + userEvent.click(canvas.getByText(args.dialogClose.text)); + }, +}; From 74a9b556c7ba6e1f0a530304364bba969a63d6c9 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 18 May 2023 21:55:44 +0200 Subject: [PATCH 012/154] fix(dialog:a11y): replce role=button with section element --- packages/kit-headless/src/components/dialog/dialog.close.tsx | 4 ++-- .../kit-headless/src/components/dialog/dialog.trigger.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.close.tsx b/packages/kit-headless/src/components/dialog/dialog.close.tsx index f369b6b19..b73902166 100644 --- a/packages/kit-headless/src/components/dialog/dialog.close.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.close.tsx @@ -10,8 +10,8 @@ export const Close = component$(() => { ); return ( -
+
-
+ ); }); diff --git a/packages/kit-headless/src/components/dialog/dialog.trigger.tsx b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx index 326b7b673..cd952b766 100644 --- a/packages/kit-headless/src/components/dialog/dialog.trigger.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx @@ -10,8 +10,8 @@ export const Trigger = component$(() => { ); return ( -
+
-
+ ); }); From 6499f88d30003c751a56522b199b2853d4d4fa1b Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 18 May 2023 21:58:01 +0200 Subject: [PATCH 013/154] refactor(dialog): set story title automatically --- packages/kit-headless/src/components/dialog/dialog.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 7ce03bae9..9d0f6ab67 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -4,7 +4,6 @@ import { Meta, StoryObj } from 'storybook-framework-qwik'; import * as Dialog from './public_api'; const meta: Meta = { - title: 'Dialog', component: Dialog.Root, }; From 8e8536232e3657c87599e7c1beac882320b7aa8c Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 18 May 2023 22:25:09 +0200 Subject: [PATCH 014/154] fix(dialog): export dialog from headless library --- packages/kit-headless/src/index.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/kit-headless/src/index.ts b/packages/kit-headless/src/index.ts index 06cb3b7d3..38793e2ff 100644 --- a/packages/kit-headless/src/index.ts +++ b/packages/kit-headless/src/index.ts @@ -1,28 +1,31 @@ export * from './components/accordion/'; +export * from './components/autocomplete'; +export * from './components/autocomplete/'; +export * from './components/autocomplete/autocomplete-root'; export * from './components/badge/badge'; +export * from './components/breadcrumb'; export * from './components/button-group/button-group'; export * from './components/card'; -export * from './components/autocomplete'; -export * from './components/combobox'; export * as Carousel from './components/carousel/carousel'; export * from './components/carousel/use'; -export * from './components/pagination/pagination'; +export * as Checkbox from './components/checkbox/checkbox'; export * from './components/collapse/collapse'; -export * as Dialog from './components/dialog/dialog' +export * from './components/combobox'; +export * from './components/combobox/'; +export * as Dialog from './components/dialog/public_api'; export * from './components/drawer'; export * from './components/input-phone'; export * as Input from './components/input/input'; -export * from './components/spinner/spinner'; export * from './components/menu/menu'; +export * from './components/navigation-bar/navigation-bar'; +export * from './components/pagination/pagination'; export * from './components/popover'; +export * from './components/qwik-ui-provider/qwik-ui-provider'; export * from './components/rating/rating'; -export * from './components/tabs'; -export * from './components/tooltip/tooltip'; -export * as Checkbox from './components/checkbox/checkbox'; -export * as CheckboxProps from './components/checkbox/checkbox'; export * from './components/select'; export * from './components/separator/separator'; export * from './components/slider'; -export * from './components/breadcrumb'; -export * from './components/navigation-bar/navigation-bar'; -export * from './components/qwik-ui-provider/qwik-ui-provider'; +export * from './components/spinner/spinner'; +export * from './components/tabs'; +export * from './components/tooltip/tooltip'; + From cedd23f48d1489dfe7082099b349da1c9d3cffec Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 18 May 2023 22:48:09 +0200 Subject: [PATCH 015/154] docs(dialog): explain why the storybook test is not valuable --- .../src/components/dialog/dialog.stories.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 9d0f6ab67..b9d0d95a8 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -39,6 +39,15 @@ export const Primary: Story = { ), play: ({ canvasElement, args }) => { + /** + * + * TODO: This test does not provide a real value. + * + * It just checks for the existence in the DOM, but not if it is visible. + * Using `toBeVisible` does not work either because the matcher does not + * seem to be capable of detecting the visibility of a HTML-Dialog. :-( + * + */ const canvas = within(canvasElement); userEvent.click(canvas.getByText(args.dialogTrigger.text)); expect(canvas.getByText(args.dialogPortal.text)).toBeTruthy(); From 253fd5848b9ecd19595e5591e1499bc7307c97b5 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 20 May 2023 21:23:46 +0200 Subject: [PATCH 016/154] feat(dialog): add Dialog.Actions --- .../src/components/dialog/dialog.actions.tsx | 16 +++++++ .../src/components/dialog/dialog.stories.tsx | 43 ++++++++++++++++--- .../src/components/dialog/public_api.ts | 1 + 3 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 packages/kit-headless/src/components/dialog/dialog.actions.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.actions.tsx b/packages/kit-headless/src/components/dialog/dialog.actions.tsx new file mode 100644 index 000000000..b28434322 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.actions.tsx @@ -0,0 +1,16 @@ +import { Slot, component$, useStyles$ } from '@builder.io/qwik'; + +export const Actions = component$(() => { + useStyles$(` + .dialog-actions { + position: sticky; + bottom: 0; + } + `); + + return ( +
+ +
+ ); +}); diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index b9d0d95a8..9163cbc3e 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -5,13 +5,6 @@ import * as Dialog from './public_api'; const meta: Meta = { component: Dialog.Root, -}; - -type Story = StoryObj; - -export default meta; - -export const Primary: Story = { args: { dialogTrigger: { text: 'Open Dialog', @@ -38,6 +31,13 @@ export const Primary: Story = { ), +}; + +type Story = StoryObj; + +export default meta; + +export const Primary: Story = { play: ({ canvasElement, args }) => { /** * @@ -54,3 +54,32 @@ export const Primary: Story = { userEvent.click(canvas.getByText(args.dialogClose.text)); }, }; + +export const ScrollingLongContent: Story = { + args: { + ...Primary.args, + dialogPortal: { + text: Array(500) + .fill(null) + .map(() => 'Hello World') + .join(' '), + }, + }, + render: (args) => ( + <> + + + + + + {args.dialogPortal.text} + + + + + + + + + ), +}; diff --git a/packages/kit-headless/src/components/dialog/public_api.ts b/packages/kit-headless/src/components/dialog/public_api.ts index 6e2c73a3c..234dae729 100644 --- a/packages/kit-headless/src/components/dialog/public_api.ts +++ b/packages/kit-headless/src/components/dialog/public_api.ts @@ -1,3 +1,4 @@ +export * from './dialog.actions'; export * from './dialog.close'; export * from './dialog.context'; export * from './dialog.portal'; From a8470ed2fb582895504a5ef8afd2d6f1d7d6fb7c Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 20 May 2023 21:57:50 +0200 Subject: [PATCH 017/154] feat(dialog): support full-screen mode --- .../src/components/dialog/dialog.portal.tsx | 11 +++++++++++ .../src/components/dialog/dialog.root.tsx | 11 ++++++++--- .../src/components/dialog/dialog.stories.tsx | 14 +++++++++++++- .../src/components/dialog/types/dialog-state.ts | 1 + 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.portal.tsx b/packages/kit-headless/src/components/dialog/dialog.portal.tsx index aaaf24ce6..bef7f4e9a 100644 --- a/packages/kit-headless/src/components/dialog/dialog.portal.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.portal.tsx @@ -3,17 +3,28 @@ import { Slot, component$, useContext, + useStylesScoped$, } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; type PortalProps = QwikIntrinsicElements['dialog']; export const Portal = component$((props: PortalProps) => { + useStylesScoped$(` + .full-screen { + width: 100vw; + height: 100vh; + } + `); + const context = useContext(dialogContext); return ( diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index 236514252..82f8ce786 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -8,10 +8,15 @@ import { useStore, } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; -import { DialogContext } from './types'; +import { DialogContext, DialogState } from './types'; -export const Root = component$(() => { - const state = useStore({ +export type RootProps = { + fullScreen?: boolean; +}; + +export const Root = component$((props: RootProps) => { + const state = useStore({ + fullScreen: props.fullScreen || false, opened: false, dialogRef: useSignal(), }); diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 9163cbc3e..6adfcf610 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -6,6 +6,9 @@ import * as Dialog from './public_api'; const meta: Meta = { component: Dialog.Root, args: { + dialog: { + fullScreen: false, + }, dialogTrigger: { text: 'Open Dialog', }, @@ -18,7 +21,7 @@ const meta: Meta = { }, render: (args) => ( <> - + @@ -83,3 +86,12 @@ export const ScrollingLongContent: Story = { ), }; + +export const FullScreen: Story = { + args: { + ...Primary.args, + dialog: { + fullScreen: true, + }, + }, +}; diff --git a/packages/kit-headless/src/components/dialog/types/dialog-state.ts b/packages/kit-headless/src/components/dialog/types/dialog-state.ts index e1b088371..ba0a3a505 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-state.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-state.ts @@ -1,6 +1,7 @@ import { Signal } from '@builder.io/qwik'; export type DialogState = { + fullScreen: boolean; opened: boolean; dialogRef: Signal; }; From 4b14485fa63d9fae0eca80101b5dfedc29e85bfd Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 20 May 2023 22:06:11 +0200 Subject: [PATCH 018/154] refactor(dialog): rename Portal to Content --- .../docs/headless/(components)/dialog/examples.tsx | 2 +- .../docs/headless/(components)/dialog/index.mdx | 4 ++-- .../docs/tailwind/(components)/dialog/examples.tsx | 4 ++-- .../docs/tailwind/(components)/dialog/index.mdx | 4 ++-- .../dialog/{dialog.portal.tsx => dialog.content.tsx} | 4 ++-- .../src/components/dialog/dialog.spec.tsx | 12 ++++++------ .../src/components/dialog/dialog.stories.tsx | 8 ++++---- .../kit-headless/src/components/dialog/public_api.ts | 2 +- .../kit-tailwind/src/components/dialog/dialog.tsx | 4 ++-- 9 files changed, 22 insertions(+), 22 deletions(-) rename packages/kit-headless/src/components/dialog/{dialog.portal.tsx => dialog.content.tsx} (83%) diff --git a/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx index 6662514dd..6f6c23abc 100644 --- a/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/dialog/examples.tsx @@ -10,7 +10,7 @@ export const Example01 = component$(() => { - Hello World + Hello World diff --git a/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx b/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx index 2933234ed..9b902fd57 100644 --- a/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/dialog/index.mdx @@ -18,11 +18,11 @@ import { KeyboardInteractionTable } from '../../../../../components/keyboard-int - +

Hello World

-
+
``` diff --git a/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx index 294e72274..140377e56 100644 --- a/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx +++ b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx @@ -10,12 +10,12 @@ export const Example01 = component$(() => { - + Hello World - + diff --git a/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx b/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx index 7a1a38844..cae0dca6d 100644 --- a/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx +++ b/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx @@ -18,12 +18,12 @@ import { KeyboardInteractionTable } from '../../../../../components/keyboard-int - +

Hello World

-
+ ``` diff --git a/packages/kit-headless/src/components/dialog/dialog.portal.tsx b/packages/kit-headless/src/components/dialog/dialog.content.tsx similarity index 83% rename from packages/kit-headless/src/components/dialog/dialog.portal.tsx rename to packages/kit-headless/src/components/dialog/dialog.content.tsx index bef7f4e9a..47121a3f9 100644 --- a/packages/kit-headless/src/components/dialog/dialog.portal.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.content.tsx @@ -7,9 +7,9 @@ import { } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; -type PortalProps = QwikIntrinsicElements['dialog']; +type ContentProps = QwikIntrinsicElements['dialog']; -export const Portal = component$((props: PortalProps) => { +export const Content = component$((props: ContentProps) => { useStylesScoped$(` .full-screen { width: 100vw; diff --git a/packages/kit-headless/src/components/dialog/dialog.spec.tsx b/packages/kit-headless/src/components/dialog/dialog.spec.tsx index a0bb31a6e..6e10b87a3 100644 --- a/packages/kit-headless/src/components/dialog/dialog.spec.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.spec.tsx @@ -8,12 +8,12 @@ describe('Dialog', () => { - + {/* Dialog.Header */} {/* Dialog.Content */} {/* Dialog.Actions/Footer */}

Hello World!

-
+ ); @@ -30,9 +30,9 @@ describe('Dialog', () => { - +

Hello World!

-
+ ); @@ -49,9 +49,9 @@ describe('Dialog', () => { - +

Hello World!

-
+ ); diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 6adfcf610..11c503a43 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -25,12 +25,12 @@ const meta: Meta = { - + {args.dialogPortal.text} - + ), @@ -74,14 +74,14 @@ export const ScrollingLongContent: Story = { - + {args.dialogPortal.text} - + ), diff --git a/packages/kit-headless/src/components/dialog/public_api.ts b/packages/kit-headless/src/components/dialog/public_api.ts index 234dae729..45b747882 100644 --- a/packages/kit-headless/src/components/dialog/public_api.ts +++ b/packages/kit-headless/src/components/dialog/public_api.ts @@ -1,6 +1,6 @@ export * from './dialog.actions'; export * from './dialog.close'; +export * from './dialog.content'; export * from './dialog.context'; -export * from './dialog.portal'; export * from './dialog.root'; export * from './dialog.trigger'; diff --git a/packages/kit-tailwind/src/components/dialog/dialog.tsx b/packages/kit-tailwind/src/components/dialog/dialog.tsx index 7cabdeb45..965f5ffcb 100644 --- a/packages/kit-tailwind/src/components/dialog/dialog.tsx +++ b/packages/kit-tailwind/src/components/dialog/dialog.tsx @@ -9,8 +9,8 @@ export const Close = Dialog.Close; export const Portal = component$(() => { return ( - + - + ); }); From 64997dcf1093685f4b0323bee8f0116def335222 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 20 May 2023 22:13:36 +0200 Subject: [PATCH 019/154] feat(dialog): all dialog overrides are passed via Dialog.Root --- .../src/components/dialog/dialog.content.tsx | 6 ++---- .../kit-headless/src/components/dialog/dialog.root.tsx | 8 ++++++-- .../kit-headless/src/components/dialog/dialog.title.tsx | 9 +++++++++ .../src/components/dialog/types/dialog-context.ts | 4 +++- 4 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 packages/kit-headless/src/components/dialog/dialog.title.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.content.tsx b/packages/kit-headless/src/components/dialog/dialog.content.tsx index 47121a3f9..b458317ff 100644 --- a/packages/kit-headless/src/components/dialog/dialog.content.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.content.tsx @@ -1,5 +1,4 @@ import { - QwikIntrinsicElements, Slot, component$, useContext, @@ -7,9 +6,7 @@ import { } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; -type ContentProps = QwikIntrinsicElements['dialog']; - -export const Content = component$((props: ContentProps) => { +export const Content = component$(() => { useStylesScoped$(` .full-screen { width: 100vw; @@ -18,6 +15,7 @@ export const Content = component$((props: ContentProps) => { `); const context = useContext(dialogContext); + const props = context.dialogProps; return ( { + const { fullScreen, ...dialogProps } = props; + const state = useStore({ - fullScreen: props.fullScreen || false, + fullScreen: fullScreen || false, opened: false, dialogRef: useSignal(), }); @@ -53,6 +56,7 @@ export const Root = component$((props: RootProps) => { ); const context: DialogContext = { + dialogProps, state, open: openDialog$, diff --git a/packages/kit-headless/src/components/dialog/dialog.title.tsx b/packages/kit-headless/src/components/dialog/dialog.title.tsx new file mode 100644 index 000000000..7fd1e2f51 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.title.tsx @@ -0,0 +1,9 @@ +import { Slot, component$ } from '@builder.io/qwik'; + +export const Title = component$(() => { + return ( + + + + ); +}); diff --git a/packages/kit-headless/src/components/dialog/types/dialog-context.ts b/packages/kit-headless/src/components/dialog/types/dialog-context.ts index 91816feb4..e0ec858e9 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-context.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-context.ts @@ -1,7 +1,9 @@ -import { QRL, QwikMouseEvent } from '@builder.io/qwik'; +import { QRL, QwikIntrinsicElements, QwikMouseEvent } from '@builder.io/qwik'; import { DialogState } from './dialog-state'; export type DialogContext = { + dialogProps: QwikIntrinsicElements['dialog']; + state: DialogState; open: QRL<() => void>; From 61ace39bd7ffa6597364f80d50a8f9620959f949 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sat, 20 May 2023 22:24:03 +0200 Subject: [PATCH 020/154] feat(dialog): add Dialog.Title --- .../src/components/dialog/dialog.stories.tsx | 35 ++++++++++++++++--- .../src/components/dialog/dialog.title.tsx | 8 +++-- .../src/components/dialog/public_api.ts | 1 + 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 11c503a43..66f442132 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -12,7 +12,8 @@ const meta: Meta = { dialogTrigger: { text: 'Open Dialog', }, - dialogPortal: { + dialogContent: { + title: 'Dialog Title', text: 'Hello World', }, dialogClose: { @@ -26,7 +27,7 @@ const meta: Meta = { - {args.dialogPortal.text} + {args.dialogContent.text} @@ -53,7 +54,7 @@ export const Primary: Story = { */ const canvas = within(canvasElement); userEvent.click(canvas.getByText(args.dialogTrigger.text)); - expect(canvas.getByText(args.dialogPortal.text)).toBeTruthy(); + expect(canvas.getByText(args.dialogContent.text)).toBeTruthy(); userEvent.click(canvas.getByText(args.dialogClose.text)); }, }; @@ -61,7 +62,7 @@ export const Primary: Story = { export const ScrollingLongContent: Story = { args: { ...Primary.args, - dialogPortal: { + dialogContent: { text: Array(500) .fill(null) .map(() => 'Hello World') @@ -75,7 +76,31 @@ export const ScrollingLongContent: Story = { - {args.dialogPortal.text} + {args.dialogContent.text} + + + + + + + + + ), +}; + +export const Title: Story = { + args: { + ...Primary.args, + }, + render: (args) => ( + <> + + + + + + {args.dialogContent.title} + {args.dialogContent.text} diff --git a/packages/kit-headless/src/components/dialog/dialog.title.tsx b/packages/kit-headless/src/components/dialog/dialog.title.tsx index 7fd1e2f51..5bdac21b4 100644 --- a/packages/kit-headless/src/components/dialog/dialog.title.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.title.tsx @@ -1,8 +1,12 @@ -import { Slot, component$ } from '@builder.io/qwik'; +import { Slot, component$, useContext } from '@builder.io/qwik'; +import { dialogContext } from './dialog.context'; export const Title = component$(() => { + const context = useContext(dialogContext); + const ariaLabeledBy = context.dialogProps['aria-labelledby']; + return ( - + ); diff --git a/packages/kit-headless/src/components/dialog/public_api.ts b/packages/kit-headless/src/components/dialog/public_api.ts index 45b747882..598f368e2 100644 --- a/packages/kit-headless/src/components/dialog/public_api.ts +++ b/packages/kit-headless/src/components/dialog/public_api.ts @@ -3,4 +3,5 @@ export * from './dialog.close'; export * from './dialog.content'; export * from './dialog.context'; export * from './dialog.root'; +export * from './dialog.title'; export * from './dialog.trigger'; From 05913942e1582951116f26bba220ce5f9c67cd42 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 12:58:53 +0200 Subject: [PATCH 021/154] feat(dialog): stop passing through every dialog-property --- packages/kit-headless/src/components/dialog/dialog.root.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index 3b3c9c0c3..41b6787ef 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -11,7 +11,10 @@ import { import { dialogContext } from './dialog.context'; import { DialogContext, DialogState } from './types'; -export type RootProps = QwikIntrinsicElements['dialog'] & { +export type RootProps = Pick< + QwikIntrinsicElements['dialog'], + 'class' | 'aria-labelledby' +> & { fullScreen?: boolean; }; From 017164a5b0eca3c427120eb39e65cee158c5014f Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 13:03:46 +0200 Subject: [PATCH 022/154] feat(dialog): add ContentText --- .../src/components/dialog/dialog.content-text.tsx | 5 +++++ .../components/dialog/dialog.content-title.tsx | 5 +++++ .../src/components/dialog/dialog.root.tsx | 2 +- .../src/components/dialog/dialog.stories.tsx | 15 +++++++++++---- .../src/components/dialog/dialog.title.tsx | 13 ------------- .../src/components/dialog/public_api.ts | 3 ++- packages/kit-headless/tsconfig.editor.json | 6 +++++- 7 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 packages/kit-headless/src/components/dialog/dialog.content-text.tsx create mode 100644 packages/kit-headless/src/components/dialog/dialog.content-title.tsx delete mode 100644 packages/kit-headless/src/components/dialog/dialog.title.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.content-text.tsx b/packages/kit-headless/src/components/dialog/dialog.content-text.tsx new file mode 100644 index 000000000..7587019a9 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.content-text.tsx @@ -0,0 +1,5 @@ +import { Slot, component$ } from '@builder.io/qwik'; + +export const ContentText = component$(() => { + return ; +}); diff --git a/packages/kit-headless/src/components/dialog/dialog.content-title.tsx b/packages/kit-headless/src/components/dialog/dialog.content-title.tsx new file mode 100644 index 000000000..e7f30302c --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.content-title.tsx @@ -0,0 +1,5 @@ +import { Slot, component$ } from '@builder.io/qwik'; + +export const ContentTitle = component$(() => { + return ; +}); diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index 41b6787ef..d887f3060 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -13,7 +13,7 @@ import { DialogContext, DialogState } from './types'; export type RootProps = Pick< QwikIntrinsicElements['dialog'], - 'class' | 'aria-labelledby' + 'class' | 'aria-labelledby' | 'aria-describedby' > & { fullScreen?: boolean; }; diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 66f442132..86f251b32 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -88,19 +88,26 @@ export const ScrollingLongContent: Story = { ), }; -export const Title: Story = { +export const Aria: Story = { args: { ...Primary.args, }, render: (args) => ( <> - + - {args.dialogContent.title} - {args.dialogContent.text} + +

{args.dialogContent.title}

+
+ +

{args.dialogContent.text}

+
diff --git a/packages/kit-headless/src/components/dialog/dialog.title.tsx b/packages/kit-headless/src/components/dialog/dialog.title.tsx deleted file mode 100644 index 5bdac21b4..000000000 --- a/packages/kit-headless/src/components/dialog/dialog.title.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Slot, component$, useContext } from '@builder.io/qwik'; -import { dialogContext } from './dialog.context'; - -export const Title = component$(() => { - const context = useContext(dialogContext); - const ariaLabeledBy = context.dialogProps['aria-labelledby']; - - return ( - - - - ); -}); diff --git a/packages/kit-headless/src/components/dialog/public_api.ts b/packages/kit-headless/src/components/dialog/public_api.ts index 598f368e2..ec541b00b 100644 --- a/packages/kit-headless/src/components/dialog/public_api.ts +++ b/packages/kit-headless/src/components/dialog/public_api.ts @@ -1,7 +1,8 @@ export * from './dialog.actions'; export * from './dialog.close'; export * from './dialog.content'; +export * from './dialog.content-text'; +export * from './dialog.content-title'; export * from './dialog.context'; export * from './dialog.root'; -export * from './dialog.title'; export * from './dialog.trigger'; diff --git a/packages/kit-headless/tsconfig.editor.json b/packages/kit-headless/tsconfig.editor.json index 29d5909f4..d0e8db055 100644 --- a/packages/kit-headless/tsconfig.editor.json +++ b/packages/kit-headless/tsconfig.editor.json @@ -1,6 +1,10 @@ { "extends": "./tsconfig.json", - "include": ["**/*.ts", "**/*.tsx"], + "include": [ + "**/*.ts", + "**/*.tsx", + "src/components/dialog/dialog.content-title" + ], "compilerOptions": { "types": ["node", "jest", "@testing-library/jest-dom", "@testing-library/cypress"] } From cb578e43aa7d28381c0ad2a145a9374ec285e208 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 13:26:36 +0200 Subject: [PATCH 023/154] feat(dialog): expose props of Dialog.Root --- .../components/dialog/dialog.root.props.tsx | 8 ++++++++ .../src/components/dialog/public_api.ts | 1 + .../src/components/dialog/dialog.tsx | 19 ++++++++++--------- 3 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 packages/kit-headless/src/components/dialog/dialog.root.props.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.root.props.tsx b/packages/kit-headless/src/components/dialog/dialog.root.props.tsx new file mode 100644 index 000000000..f69bc4eb3 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/dialog.root.props.tsx @@ -0,0 +1,8 @@ +import { QwikIntrinsicElements } from '@builder.io/qwik'; + +export type RootProps = Pick< + QwikIntrinsicElements['dialog'], + 'class' | 'aria-labelledby' | 'aria-describedby' +> & { + fullScreen?: boolean; +}; diff --git a/packages/kit-headless/src/components/dialog/public_api.ts b/packages/kit-headless/src/components/dialog/public_api.ts index ec541b00b..f1d0b01e2 100644 --- a/packages/kit-headless/src/components/dialog/public_api.ts +++ b/packages/kit-headless/src/components/dialog/public_api.ts @@ -5,4 +5,5 @@ export * from './dialog.content-text'; export * from './dialog.content-title'; export * from './dialog.context'; export * from './dialog.root'; +export * from './dialog.root.props'; export * from './dialog.trigger'; diff --git a/packages/kit-tailwind/src/components/dialog/dialog.tsx b/packages/kit-tailwind/src/components/dialog/dialog.tsx index 965f5ffcb..c842de431 100644 --- a/packages/kit-tailwind/src/components/dialog/dialog.tsx +++ b/packages/kit-tailwind/src/components/dialog/dialog.tsx @@ -1,16 +1,17 @@ import { Slot, component$ } from '@builder.io/qwik'; import { Dialog } from '@qwik-ui/headless'; -export const Root = Dialog.Root; - -export const Trigger = Dialog.Trigger; - -export const Close = Dialog.Close; - -export const Portal = component$(() => { +export const Root = component$((props: Dialog.RootProps) => { return ( - + - +
); }); + +export const Trigger = Dialog.Trigger; +export const Close = Dialog.Close; + +export const Content = Dialog.Content; +export const ContentTitle = Dialog.ContentTitle; +export const ContentText = Dialog.ContentText; From f320e0c451e07c9cc9d6c69c78dd6d9d7ba6ea6a Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 13:26:53 +0200 Subject: [PATCH 024/154] refactor(dialog): add $ suffix --- .../src/components/dialog/dialog.close.tsx | 2 +- .../src/components/dialog/dialog.content.tsx | 2 +- .../src/components/dialog/dialog.root.tsx | 15 ++++----------- .../src/components/dialog/dialog.trigger.tsx | 2 +- .../src/components/dialog/types/dialog-context.ts | 6 +++--- 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.close.tsx b/packages/kit-headless/src/components/dialog/dialog.close.tsx index b73902166..23162b449 100644 --- a/packages/kit-headless/src/components/dialog/dialog.close.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.close.tsx @@ -6,7 +6,7 @@ export const Close = component$(() => { useOn( 'click', - $(() => context.close()) + $(() => context.close$()) ); return ( diff --git a/packages/kit-headless/src/components/dialog/dialog.content.tsx b/packages/kit-headless/src/components/dialog/dialog.content.tsx index b458317ff..70aa1417a 100644 --- a/packages/kit-headless/src/components/dialog/dialog.content.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.content.tsx @@ -24,7 +24,7 @@ export const Content = component$(() => { context.state.fullScreen ? `${props.class} full-screen` : props.class } ref={context.state.dialogRef} - onClick$={context.closeOnDialogClick} + onClick$={context.closeOnDialogClick$} >
diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index d887f3060..f1c21595a 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -1,6 +1,5 @@ import { $, - QwikIntrinsicElements, QwikMouseEvent, Slot, component$, @@ -9,15 +8,9 @@ import { useStore, } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; +import { RootProps } from './dialog.root.props'; import { DialogContext, DialogState } from './types'; -export type RootProps = Pick< - QwikIntrinsicElements['dialog'], - 'class' | 'aria-labelledby' | 'aria-describedby' -> & { - fullScreen?: boolean; -}; - export const Root = component$((props: RootProps) => { const { fullScreen, ...dialogProps } = props; @@ -62,9 +55,9 @@ export const Root = component$((props: RootProps) => { dialogProps, state, - open: openDialog$, - close: closeDialog$, - closeOnDialogClick: closeOnDialogClick$, + open$: openDialog$, + close$: closeDialog$, + closeOnDialogClick$: closeOnDialogClick$, }; useContextProvider(dialogContext, context); diff --git a/packages/kit-headless/src/components/dialog/dialog.trigger.tsx b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx index cd952b766..6735bf734 100644 --- a/packages/kit-headless/src/components/dialog/dialog.trigger.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx @@ -6,7 +6,7 @@ export const Trigger = component$(() => { useOn( 'click', - $(() => context.open()) + $(() => context.open$()) ); return ( diff --git a/packages/kit-headless/src/components/dialog/types/dialog-context.ts b/packages/kit-headless/src/components/dialog/types/dialog-context.ts index e0ec858e9..597e3a658 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-context.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-context.ts @@ -6,9 +6,9 @@ export type DialogContext = { state: DialogState; - open: QRL<() => void>; - close: QRL<() => void>; - closeOnDialogClick: QRL< + open$: QRL<() => void>; + close$: QRL<() => void>; + closeOnDialogClick$: QRL< ( event: QwikMouseEvent, element: HTMLDialogElement From 7713ed18a5ec83aaecbfeae12db6807b7c0447cb Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 13:55:05 +0200 Subject: [PATCH 025/154] fix(dialog): resolve lint-error concerning scope --- .../kit-headless/src/components/dialog/dialog.close.tsx | 9 ++------- .../src/components/dialog/dialog.trigger.tsx | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.close.tsx b/packages/kit-headless/src/components/dialog/dialog.close.tsx index 23162b449..f80b564f5 100644 --- a/packages/kit-headless/src/components/dialog/dialog.close.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.close.tsx @@ -1,16 +1,11 @@ -import { $, Slot, component$, useContext, useOn } from '@builder.io/qwik'; +import { Slot, component$, useContext } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; export const Close = component$(() => { const context = useContext(dialogContext); - useOn( - 'click', - $(() => context.close$()) - ); - return ( -
+
); diff --git a/packages/kit-headless/src/components/dialog/dialog.trigger.tsx b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx index 6735bf734..77ae55c2b 100644 --- a/packages/kit-headless/src/components/dialog/dialog.trigger.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.trigger.tsx @@ -1,16 +1,11 @@ -import { $, Slot, component$, useContext, useOn } from '@builder.io/qwik'; +import { Slot, component$, useContext } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; export const Trigger = component$(() => { const context = useContext(dialogContext); - useOn( - 'click', - $(() => context.open$()) - ); - return ( -
+
); From 9aba660131e9e8ff2698c7a570c6dde968d3b346 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 14:11:42 +0200 Subject: [PATCH 026/154] feat(dialog): expose public API via ref --- .../components/dialog/dialog.root.props.tsx | 8 ---- .../src/components/dialog/dialog.root.tsx | 40 ++++++++++--------- .../src/components/dialog/dialog.stories.tsx | 24 +++++++++++ .../src/components/dialog/public_api.ts | 2 +- .../components/dialog/types/dialog-context.ts | 5 ++- .../src/components/dialog/types/dialog-ref.ts | 3 ++ .../dialog/types/dialog.root.props.ts | 12 ++++++ .../src/components/dialog/types/index.ts | 1 + .../src/components/dialog/utils.tsx | 14 +++++++ 9 files changed, 80 insertions(+), 29 deletions(-) delete mode 100644 packages/kit-headless/src/components/dialog/dialog.root.props.tsx create mode 100644 packages/kit-headless/src/components/dialog/types/dialog-ref.ts create mode 100644 packages/kit-headless/src/components/dialog/types/dialog.root.props.ts create mode 100644 packages/kit-headless/src/components/dialog/utils.tsx diff --git a/packages/kit-headless/src/components/dialog/dialog.root.props.tsx b/packages/kit-headless/src/components/dialog/dialog.root.props.tsx deleted file mode 100644 index f69bc4eb3..000000000 --- a/packages/kit-headless/src/components/dialog/dialog.root.props.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { QwikIntrinsicElements } from '@builder.io/qwik'; - -export type RootProps = Pick< - QwikIntrinsicElements['dialog'], - 'class' | 'aria-labelledby' | 'aria-describedby' -> & { - fullScreen?: boolean; -}; diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index f1c21595a..eef4d3400 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -6,10 +6,12 @@ import { useContextProvider, useSignal, useStore, + useVisibleTask$, } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; -import { RootProps } from './dialog.root.props'; -import { DialogContext, DialogState } from './types'; +import { DialogContext, DialogState, RootProps } from './types'; +import { DialogRef } from './types/dialog-ref'; +import { hasDialogBackdropBeenClicked } from './utils'; export const Root = component$((props: RootProps) => { const { fullScreen, ...dialogProps } = props; @@ -20,6 +22,7 @@ export const Root = component$((props: RootProps) => { dialogRef: useSignal(), }); + /** Opens the Dialog */ const openDialog$ = $(() => { const dialog = state.dialogRef.value; @@ -33,6 +36,7 @@ export const Root = component$((props: RootProps) => { state.opened = true; }); + /** Opens the Dialog */ const closeDialog$ = $(() => { const dialog = state.dialogRef.value; @@ -46,9 +50,10 @@ export const Root = component$((props: RootProps) => { state.opened = false; }); - const closeOnDialogClick$ = $( + /** Closes the Dialog when its Backdrop is clicked */ + const closeOnBackdropClick$ = $( (event: QwikMouseEvent) => - hasBackdropBeenClicked(event) ? closeDialog$() : Promise.resolve() + hasDialogBackdropBeenClicked(event) ? closeDialog$() : Promise.resolve() ); const context: DialogContext = { @@ -57,23 +62,22 @@ export const Root = component$((props: RootProps) => { open$: openDialog$, close$: closeDialog$, - closeOnDialogClick$: closeOnDialogClick$, + closeOnDialogClick$: closeOnBackdropClick$, }; useContextProvider(dialogContext, context); - return ; -}); + /** Share public-api with its parent. */ + useVisibleTask$(() => { + if (props.ref) { + const dialogRef: DialogRef = { + open$: context.open$, + close$: context.close$, + }; -function hasBackdropBeenClicked( - event: QwikMouseEvent -) { - const rect = (event.target as HTMLDialogElement).getBoundingClientRect(); + props.ref.value = dialogRef; + } + }); - return ( - rect.left > event.clientX || - rect.right < event.clientX || - rect.top > event.clientY || - rect.bottom < event.clientY - ); -} + return ; +}); diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 86f251b32..50a593fc7 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -1,7 +1,9 @@ +import { component$, useSignal } from '@builder.io/qwik'; import { expect } from '@storybook/jest'; import { userEvent, within } from '@storybook/testing-library'; import { Meta, StoryObj } from 'storybook-framework-qwik'; import * as Dialog from './public_api'; +import { DialogRef } from './types/dialog-ref'; const meta: Meta = { component: Dialog.Root, @@ -127,3 +129,25 @@ export const FullScreen: Story = { }, }, }; + +const DialogUsingRef = component$((args: any) => { + const dialogRef = useSignal(); + + return ( + <> + + + + +

{args.dialogContent.text}

+
+
+ + ); +}); + +export const Ref: Story = { + render: (args) => , +}; diff --git a/packages/kit-headless/src/components/dialog/public_api.ts b/packages/kit-headless/src/components/dialog/public_api.ts index f1d0b01e2..422d80ae0 100644 --- a/packages/kit-headless/src/components/dialog/public_api.ts +++ b/packages/kit-headless/src/components/dialog/public_api.ts @@ -5,5 +5,5 @@ export * from './dialog.content-text'; export * from './dialog.content-title'; export * from './dialog.context'; export * from './dialog.root'; -export * from './dialog.root.props'; export * from './dialog.trigger'; +export type { RootProps } from './types/dialog.root.props'; diff --git a/packages/kit-headless/src/components/dialog/types/dialog-context.ts b/packages/kit-headless/src/components/dialog/types/dialog-context.ts index 597e3a658..7fcfc7231 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-context.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-context.ts @@ -1,8 +1,9 @@ -import { QRL, QwikIntrinsicElements, QwikMouseEvent } from '@builder.io/qwik'; +import { QRL, QwikMouseEvent } from '@builder.io/qwik'; import { DialogState } from './dialog-state'; +import { DialogIntrinsicElementProps } from './dialog.root.props'; export type DialogContext = { - dialogProps: QwikIntrinsicElements['dialog']; + dialogProps: DialogIntrinsicElementProps; state: DialogState; diff --git a/packages/kit-headless/src/components/dialog/types/dialog-ref.ts b/packages/kit-headless/src/components/dialog/types/dialog-ref.ts new file mode 100644 index 000000000..f51d4fff7 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/types/dialog-ref.ts @@ -0,0 +1,3 @@ +import { DialogContext } from './dialog-context'; + +export type DialogRef = Pick; diff --git a/packages/kit-headless/src/components/dialog/types/dialog.root.props.ts b/packages/kit-headless/src/components/dialog/types/dialog.root.props.ts new file mode 100644 index 000000000..439526717 --- /dev/null +++ b/packages/kit-headless/src/components/dialog/types/dialog.root.props.ts @@ -0,0 +1,12 @@ +import { QwikIntrinsicElements, Signal } from '@builder.io/qwik'; +import { DialogRef } from './dialog-ref'; + +export type DialogIntrinsicElementProps = Pick< + QwikIntrinsicElements['dialog'], + 'class' | 'aria-labelledby' | 'aria-describedby' +>; + +export type RootProps = DialogIntrinsicElementProps & { + fullScreen?: boolean; + ref?: Signal; +}; diff --git a/packages/kit-headless/src/components/dialog/types/index.ts b/packages/kit-headless/src/components/dialog/types/index.ts index 98be73e3c..96fdd2a4a 100644 --- a/packages/kit-headless/src/components/dialog/types/index.ts +++ b/packages/kit-headless/src/components/dialog/types/index.ts @@ -1,2 +1,3 @@ export * from './dialog-context'; export * from './dialog-state'; +export * from './dialog.root.props'; diff --git a/packages/kit-headless/src/components/dialog/utils.tsx b/packages/kit-headless/src/components/dialog/utils.tsx new file mode 100644 index 000000000..0cc531eed --- /dev/null +++ b/packages/kit-headless/src/components/dialog/utils.tsx @@ -0,0 +1,14 @@ +import { QwikMouseEvent } from '@builder.io/qwik'; + +export function hasDialogBackdropBeenClicked( + event: QwikMouseEvent +) { + const rect = (event.target as HTMLDialogElement).getBoundingClientRect(); + + return ( + rect.left > event.clientX || + rect.right < event.clientX || + rect.top > event.clientY || + rect.bottom < event.clientY + ); +} From 2485101d5741b689a1126cdb9292a96d8402400e Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 20:03:03 +0200 Subject: [PATCH 027/154] refactor(dialog): simplify story --- .../src/components/dialog/dialog.stories.tsx | 154 ++++++++---------- 1 file changed, 65 insertions(+), 89 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 50a593fc7..861efe8cf 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -5,46 +5,32 @@ import { Meta, StoryObj } from 'storybook-framework-qwik'; import * as Dialog from './public_api'; import { DialogRef } from './types/dialog-ref'; -const meta: Meta = { +const meta: Meta = { component: Dialog.Root, args: { - dialog: { - fullScreen: false, - }, - dialogTrigger: { - text: 'Open Dialog', - }, - dialogContent: { - title: 'Dialog Title', - text: 'Hello World', - }, - dialogClose: { - text: 'Close', - }, + fullScreen: false, }, - render: (args) => ( - <> - - - - - - {args.dialogContent.text} - - - - - - + render: (props) => ( + + + + + + Hello World + + + + + ), }; -type Story = StoryObj; +type Story = StoryObj; export default meta; export const Primary: Story = { - play: ({ canvasElement, args }) => { + play: ({ canvasElement }) => { /** * * TODO: This test does not provide a real value. @@ -55,82 +41,72 @@ export const Primary: Story = { * */ const canvas = within(canvasElement); - userEvent.click(canvas.getByText(args.dialogTrigger.text)); - expect(canvas.getByText(args.dialogContent.text)).toBeTruthy(); - userEvent.click(canvas.getByText(args.dialogClose.text)); + userEvent.click(canvas.getByText('Open Dialog')); + expect(canvas.getByText('Hello World')).toBeTruthy(); + userEvent.click(canvas.getByText('Close')); }, }; export const ScrollingLongContent: Story = { - args: { - ...Primary.args, - dialogContent: { - text: Array(500) - .fill(null) - .map(() => 'Hello World') - .join(' '), - }, - }, - render: (args) => ( - <> - - - - - - {args.dialogContent.text} - - - - - - - - + render: () => ( + + + + + + {Array(500) + .fill(null) + .map(() => 'Hello World') + .join(' ')} + + + + + + + ), }; export const Aria: Story = { args: { ...Primary.args, + 'aria-labelledby': 'dialog-title', + 'aria-describedby': 'dialog-text', }, - render: (args) => ( - <> - - - - - - -

{args.dialogContent.title}

-
- -

{args.dialogContent.text}

-
- - - - - -
-
- + render: (props) => ( + + + + + + +

My Dialog Title

+
+ +

Hello World

+
+ + + + + +
+
), }; export const FullScreen: Story = { args: { - ...Primary.args, - dialog: { - fullScreen: true, - }, + fullScreen: true, }, }; -const DialogUsingRef = component$((args: any) => { +/** + * Using a component$ here to be able to use `useSignal`. + * useSignal cannot be used directly inside a story's render-Function. + */ +const DialogUsingRef = component$(() => { const dialogRef = useSignal(); return ( @@ -141,7 +117,7 @@ const DialogUsingRef = component$((args: any) => { -

{args.dialogContent.text}

+

Hello World

@@ -149,5 +125,5 @@ const DialogUsingRef = component$((args: any) => { }); export const Ref: Story = { - render: (args) => , + render: () => , }; From 6d4ffc21c34dc6b63b43436e587960048ca4428e Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 21 May 2023 20:19:54 +0200 Subject: [PATCH 028/154] feat(dialog): position Dialog.ContentTitle sticky --- .../components/dialog/dialog.content-title.tsx | 15 +++++++++++++-- .../src/components/dialog/dialog.stories.tsx | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.content-title.tsx b/packages/kit-headless/src/components/dialog/dialog.content-title.tsx index e7f30302c..1b5daecc5 100644 --- a/packages/kit-headless/src/components/dialog/dialog.content-title.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.content-title.tsx @@ -1,5 +1,16 @@ -import { Slot, component$ } from '@builder.io/qwik'; +import { Slot, component$, useStyles$ } from '@builder.io/qwik'; export const ContentTitle = component$(() => { - return ; + useStyles$(` + .dialog-content-title { + position: sticky; + top: 0; + } + `); + + return ( +
+ +
+ ); }); diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 861efe8cf..5b059f966 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -54,6 +54,9 @@ export const ScrollingLongContent: Story = { + +

My Dialog Title

+
{Array(500) .fill(null) .map(() => 'Hello World') From cf6783e138d6282be5f67b1937f4409ac3ad67bd Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 23 May 2023 20:20:17 +0200 Subject: [PATCH 029/154] fix(dialog): lock scrolling when dialog is opened --- .../src/components/dialog/dialog.root.tsx | 28 ++++--- .../src/components/dialog/dialog.stories.tsx | 22 ++++++ .../src/components/dialog/types/dialog-ref.ts | 4 +- packages/kit-headless/src/index.ts | 1 + .../src/components/dialog/dialog.tsx | 76 +++++++++++++++++-- 5 files changed, 113 insertions(+), 18 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index eef4d3400..602ac722f 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -10,7 +10,6 @@ import { } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; import { DialogContext, DialogState, RootProps } from './types'; -import { DialogRef } from './types/dialog-ref'; import { hasDialogBackdropBeenClicked } from './utils'; export const Root = component$((props: RootProps) => { @@ -67,16 +66,25 @@ export const Root = component$((props: RootProps) => { useContextProvider(dialogContext, context); - /** Share public-api with its parent. */ - useVisibleTask$(() => { - if (props.ref) { - const dialogRef: DialogRef = { - open$: context.open$, - close$: context.close$, - }; + useVisibleTask$(({ track }) => { + const opened = track(() => state.opened); - props.ref.value = dialogRef; - } + // We only share the DialogRef if the dialog's parent is interested. + if (!props.ref) return; + + props.ref.value = { + opened, + open$: context.open$, + close$: context.close$, + }; + }); + + useVisibleTask$(({ track }) => { + const opened = track(() => state.opened); + + const overflow = opened ? 'hidden' : ''; + + window.document.body.style.overflow = overflow; }); return ; diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 5b059f966..9c80bd806 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -105,6 +105,28 @@ export const FullScreen: Story = { }, }; +export const PageScrollBlocking: Story = { + render: (props) => ( + <> +

This page should not be scrollable, when the dialog is open

+ + + + + +

Hello World

+ + + + + +
+
+
+ + ), +}; + /** * Using a component$ here to be able to use `useSignal`. * useSignal cannot be used directly inside a story's render-Function. diff --git a/packages/kit-headless/src/components/dialog/types/dialog-ref.ts b/packages/kit-headless/src/components/dialog/types/dialog-ref.ts index f51d4fff7..9788eaa02 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-ref.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-ref.ts @@ -1,3 +1,5 @@ import { DialogContext } from './dialog-context'; -export type DialogRef = Pick; +export type DialogRef = Pick & { + opened: boolean; +}; diff --git a/packages/kit-headless/src/index.ts b/packages/kit-headless/src/index.ts index 38793e2ff..99da3bc1c 100644 --- a/packages/kit-headless/src/index.ts +++ b/packages/kit-headless/src/index.ts @@ -13,6 +13,7 @@ export * from './components/collapse/collapse'; export * from './components/combobox'; export * from './components/combobox/'; export * as Dialog from './components/dialog/public_api'; +export type { DialogRef } from './components/dialog/types/dialog-ref'; export * from './components/drawer'; export * from './components/input-phone'; export * as Input from './components/input/input'; diff --git a/packages/kit-tailwind/src/components/dialog/dialog.tsx b/packages/kit-tailwind/src/components/dialog/dialog.tsx index c842de431..b9b5a600e 100644 --- a/packages/kit-tailwind/src/components/dialog/dialog.tsx +++ b/packages/kit-tailwind/src/components/dialog/dialog.tsx @@ -1,17 +1,79 @@ -import { Slot, component$ } from '@builder.io/qwik'; -import { Dialog } from '@qwik-ui/headless'; +import { Slot, component$, useComputed$, useSignal } from '@builder.io/qwik'; +import { Dialog, DialogRef } from '@qwik-ui/headless'; export const Root = component$((props: Dialog.RootProps) => { + const dialog = useSignal(); + + const modalClass = useComputed$(() => { + const clazz = dialog.value?.opened ? 'modal modal-open' : 'modal'; + console.log('CHANGE', dialog.value?.opened, clazz); + + return clazz; + }); + return ( - + ); }); -export const Trigger = Dialog.Trigger; export const Close = Dialog.Close; +export const Trigger = Dialog.Trigger; + +export const Content = component$(() => { + return ( + + + + ); +}); +export const ContentTitle = component$(() => { + return ( + +

+ +

+
+ ); +}); +export const ContentText = component$(() => { + return ( + +

+ +

+
+ ); +}); + +export const Actions = component$(() => { + return ( + + + + ); +}); + +/* + + + + + + + -export const Content = Dialog.Content; -export const ContentTitle = Dialog.ContentTitle; -export const ContentText = Dialog.ContentText; +*/ From a3207ee7103c8a41f48328ad65cf4bc56b679de4 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 23 May 2023 20:50:17 +0200 Subject: [PATCH 030/154] docs(dialog): document useVisibleTask$ --- .../src/components/dialog/dialog.root.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index 602ac722f..bf29c2c98 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -66,10 +66,14 @@ export const Root = component$((props: RootProps) => { useContextProvider(dialogContext, context); + /** + * + * Share the public API of the Dialog if the dialog-caller is interested. + * + */ useVisibleTask$(({ track }) => { const opened = track(() => state.opened); - // We only share the DialogRef if the dialog's parent is interested. if (!props.ref) return; props.ref.value = { @@ -79,6 +83,11 @@ export const Root = component$((props: RootProps) => { }; }); + /** + * + * Lock Scrolling on page when Dialog is opened. + * + */ useVisibleTask$(({ track }) => { const opened = track(() => state.opened); From 413588b9fb56d74dc8965a8107b6d053094bfe5c Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 23 May 2023 20:57:42 +0200 Subject: [PATCH 031/154] refactor(dialog): move backdrop-click logic --- .../src/components/dialog/dialog.content.tsx | 10 +++++++++- .../src/components/dialog/dialog.root.tsx | 11 ----------- .../src/components/dialog/types/dialog-context.ts | 8 +------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.content.tsx b/packages/kit-headless/src/components/dialog/dialog.content.tsx index 70aa1417a..326a93ed8 100644 --- a/packages/kit-headless/src/components/dialog/dialog.content.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.content.tsx @@ -1,10 +1,13 @@ import { + $, + QwikMouseEvent, Slot, component$, useContext, useStylesScoped$, } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; +import { hasDialogBackdropBeenClicked } from './utils'; export const Content = component$(() => { useStylesScoped$(` @@ -17,6 +20,11 @@ export const Content = component$(() => { const context = useContext(dialogContext); const props = context.dialogProps; + const closeOnBackdropClick$ = $( + (event: QwikMouseEvent) => + hasDialogBackdropBeenClicked(event) ? context.close$() : Promise.resolve() + ); + return ( { context.state.fullScreen ? `${props.class} full-screen` : props.class } ref={context.state.dialogRef} - onClick$={context.closeOnDialogClick$} + onClick$={closeOnBackdropClick$} > diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index bf29c2c98..b797f5106 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -1,6 +1,5 @@ import { $, - QwikMouseEvent, Slot, component$, useContextProvider, @@ -10,7 +9,6 @@ import { } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; import { DialogContext, DialogState, RootProps } from './types'; -import { hasDialogBackdropBeenClicked } from './utils'; export const Root = component$((props: RootProps) => { const { fullScreen, ...dialogProps } = props; @@ -21,7 +19,6 @@ export const Root = component$((props: RootProps) => { dialogRef: useSignal(), }); - /** Opens the Dialog */ const openDialog$ = $(() => { const dialog = state.dialogRef.value; @@ -35,7 +32,6 @@ export const Root = component$((props: RootProps) => { state.opened = true; }); - /** Opens the Dialog */ const closeDialog$ = $(() => { const dialog = state.dialogRef.value; @@ -49,19 +45,12 @@ export const Root = component$((props: RootProps) => { state.opened = false; }); - /** Closes the Dialog when its Backdrop is clicked */ - const closeOnBackdropClick$ = $( - (event: QwikMouseEvent) => - hasDialogBackdropBeenClicked(event) ? closeDialog$() : Promise.resolve() - ); - const context: DialogContext = { dialogProps, state, open$: openDialog$, close$: closeDialog$, - closeOnDialogClick$: closeOnBackdropClick$, }; useContextProvider(dialogContext, context); diff --git a/packages/kit-headless/src/components/dialog/types/dialog-context.ts b/packages/kit-headless/src/components/dialog/types/dialog-context.ts index 7fcfc7231..8727629e0 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-context.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-context.ts @@ -1,4 +1,4 @@ -import { QRL, QwikMouseEvent } from '@builder.io/qwik'; +import { QRL } from '@builder.io/qwik'; import { DialogState } from './dialog-state'; import { DialogIntrinsicElementProps } from './dialog.root.props'; @@ -9,10 +9,4 @@ export type DialogContext = { open$: QRL<() => void>; close$: QRL<() => void>; - closeOnDialogClick$: QRL< - ( - event: QwikMouseEvent, - element: HTMLDialogElement - ) => void - >; }; From 5b292b9ff195fed91ee28e33984b6a5087240305 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 25 May 2023 20:31:30 +0200 Subject: [PATCH 032/154] test(dialog): remove useless storybook-play-test --- .../src/components/dialog/dialog.stories.tsx | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.stories.tsx b/packages/kit-headless/src/components/dialog/dialog.stories.tsx index 9c80bd806..af6cf01f7 100644 --- a/packages/kit-headless/src/components/dialog/dialog.stories.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.stories.tsx @@ -1,6 +1,4 @@ import { component$, useSignal } from '@builder.io/qwik'; -import { expect } from '@storybook/jest'; -import { userEvent, within } from '@storybook/testing-library'; import { Meta, StoryObj } from 'storybook-framework-qwik'; import * as Dialog from './public_api'; import { DialogRef } from './types/dialog-ref'; @@ -29,23 +27,7 @@ type Story = StoryObj; export default meta; -export const Primary: Story = { - play: ({ canvasElement }) => { - /** - * - * TODO: This test does not provide a real value. - * - * It just checks for the existence in the DOM, but not if it is visible. - * Using `toBeVisible` does not work either because the matcher does not - * seem to be capable of detecting the visibility of a HTML-Dialog. :-( - * - */ - const canvas = within(canvasElement); - userEvent.click(canvas.getByText('Open Dialog')); - expect(canvas.getByText('Hello World')).toBeTruthy(); - userEvent.click(canvas.getByText('Close')); - }, -}; +export const Primary: Story = {}; export const ScrollingLongContent: Story = { render: () => ( From 70efddb251bc38b4585e784c58aceb7bcd0c9806 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Thu, 25 May 2023 21:48:55 +0200 Subject: [PATCH 033/154] refactor(dialog): simplify method names --- .../src/components/dialog/dialog.root.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/kit-headless/src/components/dialog/dialog.root.tsx b/packages/kit-headless/src/components/dialog/dialog.root.tsx index b797f5106..463297be5 100644 --- a/packages/kit-headless/src/components/dialog/dialog.root.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.root.tsx @@ -19,7 +19,7 @@ export const Root = component$((props: RootProps) => { dialogRef: useSignal(), }); - const openDialog$ = $(() => { + const open$ = $(() => { const dialog = state.dialogRef.value; if (!dialog) { @@ -32,7 +32,7 @@ export const Root = component$((props: RootProps) => { state.opened = true; }); - const closeDialog$ = $(() => { + const close$ = $(() => { const dialog = state.dialogRef.value; if (!dialog) { @@ -49,12 +49,18 @@ export const Root = component$((props: RootProps) => { dialogProps, state, - open$: openDialog$, - close$: closeDialog$, + open$, + close$, }; useContextProvider(dialogContext, context); + useVisibleTask$(({ track }) => { + const dialogClass = track(() => props.class); + console.log('Class changed', dialogClass); + context.dialogProps.class = dialogClass; + }); + /** * * Share the public API of the Dialog if the dialog-caller is interested. From 584fc0e9075add00e8be78b23e0d5edea2443bf1 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Sun, 28 May 2023 22:50:19 +0200 Subject: [PATCH 034/154] feat(dialog): add Example for tailwind dialog --- .../tailwind/(components)/dialog/examples.tsx | 11 +-- .../tailwind/(components)/dialog/index.mdx | 11 +-- .../src/components/dialog/dialog.content.tsx | 25 ++++++- .../src/components/dialog/dialog.root.tsx | 15 ++-- .../components/dialog/types/dialog-context.ts | 3 - .../components/dialog/types/dialog-state.ts | 3 +- .../src/components/dialog/dialog.tsx | 71 ++++++++++++------- 7 files changed, 90 insertions(+), 49 deletions(-) diff --git a/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx index 140377e56..3697d7cf3 100644 --- a/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx +++ b/apps/website/src/routes/docs/tailwind/(components)/dialog/examples.tsx @@ -11,10 +11,13 @@ export const Example01 = component$(() => { - Hello World - - - + Hello 👋 + This is a simple dialog. + + + + +
diff --git a/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx b/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx index cae0dca6d..90f122e0c 100644 --- a/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx +++ b/apps/website/src/routes/docs/tailwind/(components)/dialog/index.mdx @@ -19,10 +19,13 @@ import { KeyboardInteractionTable } from '../../../../../components/keyboard-int -

Hello World

- - - + Hello 👋 + This is a simple dialog. + + + + +
``` diff --git a/packages/kit-headless/src/components/dialog/dialog.content.tsx b/packages/kit-headless/src/components/dialog/dialog.content.tsx index 326a93ed8..d85127f10 100644 --- a/packages/kit-headless/src/components/dialog/dialog.content.tsx +++ b/packages/kit-headless/src/components/dialog/dialog.content.tsx @@ -5,6 +5,7 @@ import { component$, useContext, useStylesScoped$, + useVisibleTask$, } from '@builder.io/qwik'; import { dialogContext } from './dialog.context'; import { hasDialogBackdropBeenClicked } from './utils'; @@ -18,18 +19,38 @@ export const Content = component$(() => { `); const context = useContext(dialogContext); - const props = context.dialogProps; + const props = context.state.props; const closeOnBackdropClick$ = $( (event: QwikMouseEvent) => hasDialogBackdropBeenClicked(event) ? context.close$() : Promise.resolve() ); + /** + * + * When dialog is closed by pressing the Escape-Key, + * we set the opened state to false. + * + */ + useVisibleTask$(() => { + const dialog = context.state.dialogRef.value; + + if (!dialog) { + throw new Error( + '[Qwik UI Dialog]: Cannot update the Dialog state. -Element not found.' + ); + } + + dialog.addEventListener('close', () => (context.state.opened = false)); + }); + return ( { - const { fullScreen, ...dialogProps } = props; - const state = useStore({ - fullScreen: fullScreen || false, + props, opened: false, dialogRef: useSignal(), }); @@ -46,7 +44,6 @@ export const Root = component$((props: RootProps) => { }); const context: DialogContext = { - dialogProps, state, open$, @@ -55,11 +52,11 @@ export const Root = component$((props: RootProps) => { useContextProvider(dialogContext, context); - useVisibleTask$(({ track }) => { - const dialogClass = track(() => props.class); - console.log('Class changed', dialogClass); - context.dialogProps.class = dialogClass; - }); + // useVisibleTask$(({ track }) => { + // const dialogClass = track(() => props.class); + // console.log('Class changed', dialogClass); + // context.dialogProps.class = dialogClass; + // }); /** * diff --git a/packages/kit-headless/src/components/dialog/types/dialog-context.ts b/packages/kit-headless/src/components/dialog/types/dialog-context.ts index 8727629e0..203e99184 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-context.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-context.ts @@ -1,10 +1,7 @@ import { QRL } from '@builder.io/qwik'; import { DialogState } from './dialog-state'; -import { DialogIntrinsicElementProps } from './dialog.root.props'; export type DialogContext = { - dialogProps: DialogIntrinsicElementProps; - state: DialogState; open$: QRL<() => void>; diff --git a/packages/kit-headless/src/components/dialog/types/dialog-state.ts b/packages/kit-headless/src/components/dialog/types/dialog-state.ts index ba0a3a505..df900ca69 100644 --- a/packages/kit-headless/src/components/dialog/types/dialog-state.ts +++ b/packages/kit-headless/src/components/dialog/types/dialog-state.ts @@ -1,7 +1,8 @@ import { Signal } from '@builder.io/qwik'; +import { RootProps } from './dialog.root.props'; export type DialogState = { - fullScreen: boolean; + props: RootProps; opened: boolean; dialogRef: Signal; }; diff --git a/packages/kit-tailwind/src/components/dialog/dialog.tsx b/packages/kit-tailwind/src/components/dialog/dialog.tsx index b9b5a600e..0a3df1929 100644 --- a/packages/kit-tailwind/src/components/dialog/dialog.tsx +++ b/packages/kit-tailwind/src/components/dialog/dialog.tsx @@ -1,20 +1,56 @@ -import { Slot, component$, useComputed$, useSignal } from '@builder.io/qwik'; +import { + Slot, + component$, + useComputed$, + useSignal, + useStyles$, +} from '@builder.io/qwik'; import { Dialog, DialogRef } from '@qwik-ui/headless'; export const Root = component$((props: Dialog.RootProps) => { + /** + * + * We use DaisyUI's css classes for modal to style the dialog. + * DaisyUI builds on top of Div-Elements. We use the -Element. + * + * That's why we need to move the backdrop styling to the dialog's + * backdrop-pseudo-element. + * + */ + useStyles$(` + .modal--backdrop { + background-color: initial; + } + + .modal--backdrop::backdrop { + background-color: hsl(var(--nf, var(--n)) / var(--tw-bg-opacity)); + + /* copied from daisy-ui modal */ + + --n: 218.18 18.033% 11.961%; + --nf: 222.86 17.073% 8.0392%; + --tw-bg-opacity: .4; + } + `); + const dialog = useSignal(); - const modalClass = useComputed$(() => { + const dialogClass = useComputed$(() => { const clazz = dialog.value?.opened ? 'modal modal-open' : 'modal'; - console.log('CHANGE', dialog.value?.opened, clazz); return clazz; }); return ( - - - + <> + + + + ); }); @@ -30,6 +66,7 @@ export const Content = component$(() => {
); }); + export const ContentTitle = component$(() => { return ( @@ -39,6 +76,7 @@ export const ContentTitle = component$(() => { ); }); + export const ContentText = component$(() => { return ( @@ -52,28 +90,9 @@ export const ContentText = component$(() => { export const Actions = component$(() => { return ( -
- + ); diff --git a/apps/website/src/routes/docs/_components/preview-code-example/preview-code-example-vertical.tsx b/apps/website/src/routes/docs/_components/preview-code-example/preview-code-example-vertical.tsx index ef61011c0..4a7645621 100644 --- a/apps/website/src/routes/docs/_components/preview-code-example/preview-code-example-vertical.tsx +++ b/apps/website/src/routes/docs/_components/preview-code-example/preview-code-example-vertical.tsx @@ -4,7 +4,7 @@ import { PreviewCodeExampleProps } from './preview-code-example-props.type'; export const PreviewCodeExampleVertical = component$((props: PreviewCodeExampleProps) => { return ( -
+

@@ -13,7 +13,7 @@ export const PreviewCodeExampleVertical = component$((props: PreviewCodeExampleP Code
); diff --git a/apps/website/src/routes/docs/_components/status-banner/status-banner.tsx b/apps/website/src/routes/docs/_components/status-banner/status-banner.tsx index 0cdddf8dd..5c4852d9a 100644 --- a/apps/website/src/routes/docs/_components/status-banner/status-banner.tsx +++ b/apps/website/src/routes/docs/_components/status-banner/status-banner.tsx @@ -69,12 +69,12 @@ function getBackgroundByStatus(status?: ComponentStatus) { case ComponentStatus.Ready: return 'bg-green-300'; case ComponentStatus.Beta: - return 'bg-qwikui-blue-800 dark:bg-qwikui-purple-800'; + return 'bg-gradient-to-b from-qwikui-blue-800 to-qwikui-blue-900 dark:from-qwikui-purple-800 dark:to-qwikui-purple-900'; case ComponentStatus.Draft: - return 'bg-orange-700 dark:bg-red-800'; + return 'bg-gradient-to-b from-orange-700 to-orange-800 dark:from-red-700 dark:to-red-800'; case ComponentStatus.Planned: default: - return 'bg-orange-700 dark:bg-red-800'; + return 'bg-gradient-to-b from-orange-700 to-orange-800 dark:from-red-700 dark:to-red-800'; } } diff --git a/apps/website/src/routes/docs/fluffy/(components)/navigation-bar/examples.tsx b/apps/website/src/routes/docs/fluffy/(components)/navigation-bar/examples.tsx deleted file mode 100644 index a837db778..000000000 --- a/apps/website/src/routes/docs/fluffy/(components)/navigation-bar/examples.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { component$ } from '@builder.io/qwik'; -import { NavigationBar } from '@qwik-ui/tailwind'; - -export default component$(() => { - return ( - <> -

This is the documentation for the Navigation Bar

- -
- - ); -}); diff --git a/apps/website/src/routes/docs/headless/(components)/combobox/examples.tsx b/apps/website/src/routes/docs/headless/(components)/combobox/examples.tsx index 39846adb2..65045f8e4 100644 --- a/apps/website/src/routes/docs/headless/(components)/combobox/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/combobox/examples.tsx @@ -1,41 +1,63 @@ import { component$, type JSXNode } from '@builder.io/qwik'; -import { CodeExample } from '../../../_components/code-example/code-example'; import { Highlight } from '../../../_components/highlight/highlight'; -import { PreviewCodeExampleTabsDeprecated } from '../../../_components/preview-code-example/preview-code-example-tabs-deprecated'; +import { CodeExampleContainer } from '../../../_components/code-example/code-example-container'; +import { PreviewCodeExampleTabs } from '../../../_components/preview-code-example/preview-code-example-tabs'; + import AnimationComponent from './examples/animation'; import animationCode from './examples/animation?raw'; + import AutoPlacementComponent from './examples/auto-placement'; import autoPlacementCode from './examples/auto-placement?raw'; -import buildingBlocksCode from './examples/building-blocks?raw'; + +import buildingBlocksCode from './examples/building-blocks-snip?raw'; +import providerCode from './examples/qwik-ui-provider-snip?raw'; +import contextIdsCode from './examples/context-ids-snip?raw'; +import resolvedOptionCode from './examples/resolved-option-snip?raw'; +import customKeysSnipCode from './examples/custom-keys-snip?raw'; + import CustomFilterComponent from './examples/custom-filter'; import customFilterCode from './examples/custom-filter?raw'; + import CustomKeysComponent from './examples/custom-keys'; import customKeysCode from './examples/custom-keys?raw'; + import DefaultLabelComponent from './examples/default-label'; import defaultLabelCode from './examples/default-label?raw'; + import DisableBlurComponent from './examples/disable-blur'; import disableBlurCode from './examples/disable-blur?raw'; + import DisabledComponent from './examples/disabled'; import disabledCode from './examples/disabled?raw'; + import FlipComponent from './examples/flip'; import flipCode from './examples/flip?raw'; + import GutterComponent from './examples/gutter'; import gutterCode from './examples/gutter?raw'; + import HeroComponent from './examples/hero'; import heroCode from './examples/hero?raw'; + import HighlightedIndexComponent from './examples/highlighted-index'; import highlightedIndexCode from './examples/highlighted-index?raw'; + import ObjectComponent from './examples/object'; import objectCode from './examples/object?raw'; + import PlacementComponent from './examples/placement'; import placementCode from './examples/placement?raw'; + import SearchBarComponent from './examples/search-bar'; import searchBarCode from './examples/search-bar?raw'; + import SignalBindsComponent from './examples/signal-binds'; import signalBindsCode from './examples/signal-binds?raw'; + import SortFilterComponent from './examples/sort-filter'; import sortFilterCode from './examples/sort-filter?raw'; + import StringComponent from './examples/string'; import stringCode from './examples/string?raw'; @@ -123,18 +145,32 @@ export type ShowExampleProps = { export const ShowExample = component$(({ example }: ShowExampleProps) => { const { component, code, cssClasses = '' } = comboboxExamples[example]; return ( - +
{component}
-
+ ); }); -export const BuildingBlocks = component$(() => ( - - - +export const BuildingBlocksSnip = component$(() => ( + +)); + +export const ProviderSnip = component$(() => ( + +)); + +export const ContextIdsSnip = component$(() => ( + +)); + +export const ResolvedOptionSnip = component$(() => ( + +)); + +export const CustomKeysSnip = component$(() => ( + )); diff --git a/apps/website/src/routes/docs/headless/(components)/combobox/examples/building-blocks.tsx b/apps/website/src/routes/docs/headless/(components)/combobox/examples/building-blocks-snip.tsx similarity index 100% rename from apps/website/src/routes/docs/headless/(components)/combobox/examples/building-blocks.tsx rename to apps/website/src/routes/docs/headless/(components)/combobox/examples/building-blocks-snip.tsx diff --git a/apps/website/src/routes/docs/headless/(components)/combobox/examples/context-ids-snip.tsx b/apps/website/src/routes/docs/headless/(components)/combobox/examples/context-ids-snip.tsx new file mode 100644 index 000000000..174b3d1fc --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/combobox/examples/context-ids-snip.tsx @@ -0,0 +1,10 @@ +import { component$ } from '@builder.io/qwik'; +import { ComboboxPortal } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + <> + + ); +}); diff --git a/apps/website/src/routes/docs/headless/(components)/combobox/examples/custom-keys-snip.tsx b/apps/website/src/routes/docs/headless/(components)/combobox/examples/custom-keys-snip.tsx new file mode 100644 index 000000000..1a770a54f --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/combobox/examples/custom-keys-snip.tsx @@ -0,0 +1,30 @@ +import { Combobox } from '@qwik-ui/headless'; +import { component$ } from '@builder.io/qwik'; + +type Pokemon = { + pokedex: string; + pokemon: string; + isPokemonCaught: boolean; +}; + +const pokemonExample: Array = [ + { pokedex: '1', pokemon: 'Bulbasaur', isPokemonCaught: true }, + { pokedex: '2', pokemon: 'Ivysaur', isPokemonCaught: false }, + { pokedex: '3', pokemon: 'Venusaur', isPokemonCaught: false }, + { pokedex: '4', pokemon: 'Charmander', isPokemonCaught: true }, + { pokedex: '5', pokemon: 'Charmeleon', isPokemonCaught: true }, + { pokedex: '6', pokemon: 'Charizard', isPokemonCaught: true }, + { pokedex: '7', pokemon: 'Squirtle', isPokemonCaught: false }, + { pokedex: '8', pokemon: 'Wartortle', isPokemonCaught: false }, +]; + +export default component$(() => { + return ( + + ); +}); diff --git a/apps/website/src/routes/docs/headless/(components)/combobox/examples/placement.tsx b/apps/website/src/routes/docs/headless/(components)/combobox/examples/placement.tsx index 9248f891d..4bb3a22d3 100644 --- a/apps/website/src/routes/docs/headless/(components)/combobox/examples/placement.tsx +++ b/apps/website/src/routes/docs/headless/(components)/combobox/examples/placement.tsx @@ -30,7 +30,7 @@ export default component$(() => { return ( <> { + return ( + +
+ +
+
+ ); +}); diff --git a/apps/website/src/routes/docs/headless/(components)/combobox/examples/resolved-option-snip.tsx b/apps/website/src/routes/docs/headless/(components)/combobox/examples/resolved-option-snip.tsx new file mode 100644 index 000000000..54d40af1e --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/combobox/examples/resolved-option-snip.tsx @@ -0,0 +1,14 @@ +import { component$ } from '@builder.io/qwik'; +import { ComboboxOption, ComboboxListbox, type ResolvedOption } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + ( + + {option.label} + + )} + /> + ); +}); diff --git a/apps/website/src/routes/docs/headless/(components)/combobox/index.mdx b/apps/website/src/routes/docs/headless/(components)/combobox/index.mdx index b66d2f95b..c4597f991 100644 --- a/apps/website/src/routes/docs/headless/(components)/combobox/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/combobox/index.mdx @@ -10,6 +10,11 @@ import { statusByComponent, KeyboardInteractionTable, APITable, + BuildingBlocksSnip, + ProviderSnip, + ContextIdsSnip, + ResolvedOptionSnip, + CustomKeysSnip } from './exports'; @@ -52,70 +57,24 @@ Qwik UI's Combobox implementation follows the [WAI-Aria Combobox specifications]
-
-

The Combobox component makes use of portals. A basic use case for a portal is to prevent overflow issues in your UI. To support portals in Qwik UI, please add the following around your layout.tsx.

- - ```tsx - import { QwikUIProvider } from '@qwik-ui/headless'; // import Provider component - - // wrap as a direct child to the body tag - - - - - - ``` - - -
- ### **Context Caveats** -

Portals are still currently in **Beta**, as a result, you may experience an issue using your own context to pass data into the portal children.

-

If you do experience any context related issues, add the following **contextIds** prop to the **ComboboxPortal** component. It takes in an array of string context id's as a prop. We also have a live example below with context.

-

If you are not using context inside the portal children, this will not be an issue.

- - - ```tsx - - - Option - - - ``` - -
-
+The Combobox component makes use of [portals](https://qwik.builder.io/docs/cookbook/portal/#portal). A basic use case for a portal is to prevent overflow issues in your UI. To support portals in Qwik UI, please add the following around your layout.tsx. + + +## **Context Caveats** +Portals are still currently in **Beta**, as a result, you may experience an issue using your own context to pass data into the portal children. + +If you do experience any context related issues, add the following **contextIds** prop to the **ComboboxPortal** component. + + + +It takes in an array of string context id's as a prop. We also have a live example below with context. +If you are not using context inside the portal children, this will not be an issue.
## Building blocks - - ```tsx - import { component$ } from '@builder.io/qwik'; - import { Combobox, ComboboxLabel, ComboboxControl, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxListbox, ComboboxOption } from '@qwik-ui/headless'; - - export default component$(() => { - return ( - - Label Element - - - - Opens Listbox - - - - ( - - Option Label - - )} /> - - - ) - ``` - + ### 🎨 Anatomy - ```tsx - import { ComboboxOption, type ResolvedOption } from '@qwik-ui/headless'; - - renderOption$={(option: ResolvedOption, index: number) => ( - - {option.label} - - )} - ``` - + ## Adding a filter @@ -234,16 +179,7 @@ In some cases, your data object keys may not match the default keys that the Com If your data object keys are different, you can specify custom key names using the `optionValueKey` and `optionLabelKey` props. - - ```tsx - - ``` - + Within our example, the value key is called **pokedex** and the label key is called **pokemon**. This tells the Combobox component to use the "pokedex" key for option values and the "pokemon" key for option labels. @@ -658,13 +594,13 @@ The Combobox component API provides a set of properties that allow you to custom ## Additional Examples +
+ **NOTE:** You cannot use **flip** and **autoPlacement** at the same time. They both manipulate the placement but with different strategies. Using both can result in a continuous reset loop as they try to override each other's work. +
+ ### Auto Placement Automatically places the listbox based on available space. **You must set flip to false before using it.** This comes in handy when you're unsure about the optimal placement for the floating element, or if you prefer not to set it manually. -
- **NOTE:** You cannot use **flip** and **autoPlacement** at the same time. They both manipulate the placement but with different strategies. Using both can result in a continuous reset loop as they try to override each other's work. -
- diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index bc2f48464..1fdb58250 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -61,6 +61,12 @@ In Chrome and Edge, a `top layer` attribute is shown in the dev tools. This mean > The previous implementation of modals required the use of portals, the top layer pseudo class is a native solution to this problem. +## Backdrops + +Due to the modal's implementation using the dialog, the `::backdrop` pseudo element can be used as well. + + + ## Accessibility ### Keyboard interaction diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index d590effab..6ec7375ee 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -67,7 +67,9 @@ export const Modal = component$((props: ModalProps) => { // cleanup the scroll padding const currentPadding = parseInt(document.body.style.paddingRight); - document.body.style.paddingRight = `${currentPadding - scrollbarWidth.width}px`; + if (scrollbarWidth.width) { + document.body.style.paddingRight = `${currentPadding - scrollbarWidth.width}px`; + } }); }); From 3410dba02901438b29f9e1088c44deb53aeda1e2 Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Fri, 13 Oct 2023 03:34:34 -0500 Subject: [PATCH 107/154] docs(modal): updating modal docs --- .../headless/(components)/modal/examples.tsx | 26 +++++ .../modal/examples/auto-focus.tsx | 80 +++++++++++++ .../(components)/modal/examples/backdrop.css | 8 ++ .../(components)/modal/examples/backdrop.tsx | 86 ++++++++++++++ .../modal/examples/building-blocks-snip.tsx | 12 ++ .../headless/(components)/modal/exports.ts | 1 + .../headless/(components)/modal/index.mdx | 108 +++++++++++++++--- 7 files changed, 306 insertions(+), 15 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/auto-focus.tsx create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.css create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.tsx create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx index 879a18f6c..edaebf617 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx @@ -7,7 +7,17 @@ import heroExampleCode from './examples/hero?raw'; import Inspect from './examples/inspect'; import inspectExampleCode from './examples/inspect?raw'; +import AutoFocus from './examples/auto-focus'; +import autoFocusExampleCode from './examples/auto-focus?raw'; + +import Backdrop from './examples/backdrop'; +import backdropCssCode from './examples/backdrop.css?raw'; +import backdropExampleCode from './examples/backdrop?raw'; + +import buildingBlocksSnip from './examples/building-blocks-snip?raw'; + import styles from './index.css?inline'; +import { CodeExampleContainer } from '../../../_components/code-example/code-example-container'; export type Example = { component: JSXNode; @@ -24,6 +34,14 @@ export const examples: Record = { component: , code: inspectExampleCode, }, + autoFocus: { + component: , + code: autoFocusExampleCode, + }, + backdrop: { + component: , + code: backdropExampleCode, + }, }; export type ShowExampleProps = { @@ -42,3 +60,11 @@ export const ShowExample = component$(({ example }: ShowExampleProps) => { ); }); + +export const BackdropCss = component$(() => ( + +)); + +export const BuildingBlocksSnip = component$(() => ( + +)); diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/auto-focus.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/auto-focus.tsx new file mode 100644 index 000000000..5e4b545a9 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/auto-focus.tsx @@ -0,0 +1,80 @@ +import { QwikIntrinsicElements, component$, useSignal } from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; + +export default component$(() => { + const showSig = useSignal(false); + + return ( + <> + + + +

Edit Profile

+

+ You can update your profile here. Hit the save button when finished. +

+
+ +
+ + +
+
+ + +
+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.css b/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.css new file mode 100644 index 000000000..89c5fdafe --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.css @@ -0,0 +1,8 @@ +/* class we add to */ +.my-backdrop::backdrop { + /* changing background */ + background: rgba(0, 0, 0, 0.4); + + /* providing multiple filters */ + backdrop-filter: grayscale(90%) blur(10px); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.tsx new file mode 100644 index 000000000..2354ef44f --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop.tsx @@ -0,0 +1,86 @@ +import { + type QwikIntrinsicElements, + component$, + useSignal, + useStyles$, +} from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; +import styles from './backdrop.css?inline'; + +export default component$(() => { + const showSig = useSignal(false); + useStyles$(styles); + + return ( + <> + + + +

Edit Profile

+

+ You can update your profile here. Hit the save button when finished. +

+
+ +
+ + +
+
+ + +
+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx new file mode 100644 index 000000000..afcaebcae --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx @@ -0,0 +1,12 @@ +import { component$ } from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + + + + + ); +}); diff --git a/apps/website/src/routes/docs/headless/(components)/modal/exports.ts b/apps/website/src/routes/docs/headless/(components)/modal/exports.ts index d5db5f160..071517b7c 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/exports.ts +++ b/apps/website/src/routes/docs/headless/(components)/modal/exports.ts @@ -2,4 +2,5 @@ export { statusByComponent } from '../../../../../_state/component-statuses'; export { APITable } from '../../../_components/api-table/api-table'; export { KeyboardInteractionTable } from '../../../_components/keyboard-interaction-table/keyboard-interaction-table'; export { StatusBanner } from '../../../_components/status-banner/status-banner'; +export { AnatomyTable } from '../../../_components/anatomy-table/anatomy-table'; export * from './examples'; diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index 1fdb58250..f1ab8b415 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -6,11 +6,14 @@ import ImgTestImage from '/public/images/test-image.png?jsx'; import { KeyboardInteractionTable, APITable, + AnatomyTable, StatusBanner, statusByComponent, ShortExample, LongExample, - ShowExample + ShowExample, + BackdropCss, + BuildingBlocksSnip } from './exports'; import testImage from '../../../../../../public/images/test-image.png' @@ -23,13 +26,21 @@ A window overlaid on either the primary window or another dialog window. Modal c -The modal makes use of HTML's `dialog` element, which has great browser support in [every major browser](https://caniuse.com/?search=dialog). +The modal makes use of HTML's `dialog` element, which has great support in [every major browser](https://caniuse.com/?search=dialog). > For non-modal UI elements, it is preferred to use [Qwik UI's popover component](../../../docs/headless/popover/) *(in progress)*. In the near future, Qwik's popover component can be applied to the most-semantically-relevant HTML element, including the `` element itself for non-modal dialogs. Modals are used when an important choice needs to be made in the application. The rest of the content isn't interactive until a certain action has been performed. -##### ✨ Features +
+[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/modal) + +[Report an issue 🚨](https://github.com/qwikifiers/qwik-ui/issues) + +[Edit This Page 🗒️]() +
+ +## ✨ Features - Managed focus order - Scroll locking @@ -37,34 +48,93 @@ Modals are used when an important choice needs to be made in the application. Th - Closes on escape - Toggle backdrop dismiss -
-[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/modal) +## Building Blocks -[Report an issue 🚨](https://github.com/qwikifiers/qwik-ui/issues) + -[Edit This Page 🗒️]() -
+### 🎨 Anatomy + -## The dialog element +## Dialog extendability -The [dialog element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) provides a native solution for modals. That said, the element itself is not fully accessible, and may not have the desired features. Qwik UI solves that problem, providing scroll & focus lock, as well as other quality of life features on top of the dialog element. +The Qwik UI Modal component is built on top of the `dialog` element. To see additional capabilities of the dialog, take a look at the [offical MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog). + +Our goal is to fill existing gaps, ensuring your modal is a11y compliant and enriched with HTML spec features, reducing reliance on Qwik UI maintainers. + +### Attribute example -> To add dialog properties, it is the same as sticking the attribute, class, or API on the dialog element itself. + -### The top layer +Above is an example of the `autofocus` attribute used to focus the 2nd input when the dialog is opened. + +> When adding properties on ``, it is the same as sticking the attribute, class, or API on the dialog element itself. + +## Open or close the modal + +To open or close the modal, we can use `bind:show`, a custom signal bind that leverages the [bind](https://qwik.builder.io/docs/components/rendering/#bind-attribute) syntax in Qwik. + + + +`showSig` is a signal that we pass into our Modal component to customize when the modal can be open or closed. + +The above example sets the signal to true when the button with the text **Open Modal** is clicked, then closed whenever a button inside of the modal is clicked. + +> Custom signal binds can be thought of as a remote control for a television. Just as you use a remote control to change channels, adjust volume, or turn the TV on or off, custom signal binds allow you to control the state of a component, such as opening or closing a modal, from a distance. + +## The top layer -In Chrome and Edge, a `top layer` attribute is shown in the dev tools. This means dialog content is placed above any other content, preventing any overflow or style issues. +In Chrome and Edge, a `top layer` UI button is shown in the dev tools. This means the element content is placed above any other content on the page, preventing any overflow or style issues. + +The top layer is used in Qwik UI, whenever a modal or popover is used. Popovers can be applied to any semantic element, modals in Qwik UI are tied to the `dialog` element. -> The previous implementation of modals required the use of portals, the top layer pseudo class is a native solution to this problem. +> The previous implementation of modals required the use of [portals](https://qwik.builder.io/docs/cookbook/portal/#portal), the top layer pseudo class is a **native solution** to this problem. No more z-index wars! ## Backdrops -Due to the modal's implementation using the dialog, the `::backdrop` pseudo element can be used as well. +To add a modal backdrop, the `::backdrop` pseudo element can be utilized. [Backdrops](https://developer.mozilla.org/en-US/docs/Web/CSS/::backdrop) are right underneath top layer elements. + +By default, the `dialog` element comes with a subtle backdrop, below is a snippet customizing the backdrop background along with the [backdrop filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) css property. + + + + +Styling a [backdrop in Tailwind](https://tailwindcss.com/docs/backdrop-blur) can be done using the `:backdrop` selector. In the example code below, we've changed the brightness based on dark or light mode, along with a blur. + + + +## Modal focus + +One of the features that Qwik UI provides is focus locking. This refers to keeping focus within the modal element. + + + +For example, when tabbing and the modal is opened, it will loop back to the first focusable element. + +> This is a default behavior that we provide out of the box, please [let us know](https://github.com/qwikifiers/qwik-ui/issues) if there are any additional use cases that require personalized focus lock behavior. ## Accessibility @@ -77,5 +147,13 @@ Due to the modal's implementation using the dialog, the `::backdrop` pseudo elem keyTitle: 'Escape', description: 'Closes the dialog.', }, + { + keyTitle: 'Tab', + description: 'Moves focus to the next focusable item in the modal. If none, then loops back to the last item.', + }, + { + keyTitle: 'Shift + Tab', + description: 'Moves focus to the previous focusable item in the modal. If none, then loops back to the last item.', + }, ]} /> From 70c88cceae4162965318ea53170e0f5af18c1007 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 13 Oct 2023 22:36:30 +0200 Subject: [PATCH 108/154] feat(modal): allow disabling close on backdrop-click --- .../(components)/modal/examples/hero.tsx | 2 +- .../src/components/modal/modal.spec.tsx | 22 +++++++++++++++--- .../src/components/modal/modal.tsx | 23 ++++++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx index 730388bd9..f563d4867 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx @@ -1,4 +1,4 @@ -import { type QwikIntrinsicElements, component$, useSignal } from '@builder.io/qwik'; +import { component$, useSignal, type QwikIntrinsicElements } from '@builder.io/qwik'; import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; export default component$(() => { diff --git a/packages/kit-headless/src/components/modal/modal.spec.tsx b/packages/kit-headless/src/components/modal/modal.spec.tsx index 718aa345e..2e416135f 100644 --- a/packages/kit-headless/src/components/modal/modal.spec.tsx +++ b/packages/kit-headless/src/components/modal/modal.spec.tsx @@ -1,5 +1,5 @@ import { component$, useSignal } from '@builder.io/qwik'; -import { Modal } from './modal'; +import { Modal, ModalProps } from './modal'; import { ModalContent } from './modal-content'; import { ModalFooter } from './modal-footer'; import { ModalHeader } from './modal-header'; @@ -8,14 +8,14 @@ import { ModalHeader } from './modal-header'; * SUT - System under test * Reference: https://en.wikipedia.org/wiki/System_under_test */ -const Sut = component$(() => { +const Sut = component$((props?: ModalProps) => { const showSig = useSignal(false); return ( <> - +

Hello 👋

@@ -90,6 +90,22 @@ describe('Modal', () => { cy.get('dialog').should('not.be.visible'); }); + it(`Given a Modal + WHEN closing the Modal on backdrop-click is deactivated + THEN it stays open, after the backdrop has been clicked`, () => { + cy.mount(); + + cy.get('[data-test=modal-trigger]').click(); + + cy.get('body').click('top'); + + cy.get('dialog').should('be.visible'); + + cy.realPress('Escape'); + + cy.get('dialog').should('not.be.visible'); + }); + it(`GIVEN a Modal WHEN opening it AND hitting ESC on the keyboard diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 6ec7375ee..0138aa7c1 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -27,6 +27,8 @@ export type ModalProps = Omit & { onClose$?: QRL<() => void>; show?: boolean; 'bind:show'?: Signal; + closeOnBackdropClick?: boolean; + 'bind:closeOnBackdropClick'?: Signal; }; export const Modal = component$((props: ModalProps) => { @@ -34,16 +36,31 @@ export const Modal = component$((props: ModalProps) => { const scrollbarWidth: WidthState = { width: null }; const { 'bind:show': givenOpenSig, show: givenShow } = props; + const { + 'bind:closeOnBackdropClick': givenCloseOnBackdropClickSig, + closeOnBackdropClick: givenCloseOnBackdropClick, + } = props; const defaultOpenSig = useSignal(false); + const defaultCloseOnBackdropClickSig = useSignal(true); + const showSig = givenOpenSig || defaultOpenSig; + const closeOnBackdropClickSig = + givenCloseOnBackdropClickSig || defaultCloseOnBackdropClickSig; - useTask$(async function syncOpenProp({ track }) { + useTask$(async function syncShowProp({ track }) { const showPropValue = track(() => givenShow); showSig.value = showPropValue || false; }); + useTask$(async function syncShowProp({ track }) { + const closeOnBackdropClickValue = track(() => givenCloseOnBackdropClick); + + closeOnBackdropClickSig.value = + closeOnBackdropClickValue === undefined ? true : closeOnBackdropClickValue; + }); + useTask$(async function toggleModal({ track, cleanup }) { const isOpen = track(() => showSig.value); const modal = modalRefSig.value; @@ -74,6 +91,10 @@ export const Modal = component$((props: ModalProps) => { }); const closeOnBackdropClick$ = $((event: QwikMouseEvent) => { + if (!closeOnBackdropClickSig.value) { + return; + } + if (wasModalBackdropClicked(modalRefSig.value, event)) { showSig.value = false; } From 9108bd99a97d98a0201bcc6797bf40965cb6b62c Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 13 Oct 2023 22:44:38 +0200 Subject: [PATCH 109/154] docs(modal): add API table --- .../headless/(components)/modal/index.mdx | 84 ++++++++++++------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index f1ab8b415..144820188 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -26,11 +26,11 @@ A window overlaid on either the primary window or another dialog window. Modal c -The modal makes use of HTML's `dialog` element, which has great support in [every major browser](https://caniuse.com/?search=dialog). +The modal makes use of HTML's `dialog` element, which has great browser support in [every major browser](https://caniuse.com/?search=dialog). > For non-modal UI elements, it is preferred to use [Qwik UI's popover component](../../../docs/headless/popover/) *(in progress)*. In the near future, Qwik's popover component can be applied to the most-semantically-relevant HTML element, including the `` element itself for non-modal dialogs. -Modals are used when an important choice needs to be made in the application. The rest of the content isn't interactive until a certain action has been performed. +Modals are used when an important choice needs to be made in the application. The rest of the content isn't interactive until a certain action has been performed.
[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/modal) @@ -80,15 +80,15 @@ Modals are used when an important choice needs to be made in the application. Th The Qwik UI Modal component is built on top of the `dialog` element. To see additional capabilities of the dialog, take a look at the [offical MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog). -Our goal is to fill existing gaps, ensuring your modal is a11y compliant and enriched with HTML spec features, reducing reliance on Qwik UI maintainers. +> To add dialog properties, it is the same as sticking the attribute, class, or API on the dialog element itself. ### Attribute example -Above is an example of the `autofocus` attribute used to focus the 2nd input when the dialog is opened. +Above is an example of the `autofocus` attribute used to focus the 2nd input when the dialog is opened. -> When adding properties on ``, it is the same as sticking the attribute, class, or API on the dialog element itself. +> When adding properties on ``, it is the same as sticking the attribute, class, or API on the dialog element itself. ## Open or close the modal @@ -106,7 +106,7 @@ The above example sets the signal to true when the button with the text **Open M -In Chrome and Edge, a `top layer` UI button is shown in the dev tools. This means the element content is placed above any other content on the page, preventing any overflow or style issues. +In Chrome and Edge, a `top layer` attribute is shown in the dev tools. This means dialog content is placed above any other content, preventing any overflow or style issues. The top layer is used in Qwik UI, whenever a modal or popover is used. Popovers can be applied to any semantic element, modals in Qwik UI are tied to the `dialog` element. @@ -114,28 +114,7 @@ The top layer is used in Qwik UI, whenever a modal or popover is used. Popovers ## Backdrops -To add a modal backdrop, the `::backdrop` pseudo element can be utilized. [Backdrops](https://developer.mozilla.org/en-US/docs/Web/CSS/::backdrop) are right underneath top layer elements. - -By default, the `dialog` element comes with a subtle backdrop, below is a snippet customizing the backdrop background along with the [backdrop filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) css property. - - - - - -Styling a [backdrop in Tailwind](https://tailwindcss.com/docs/backdrop-blur) can be done using the `:backdrop` selector. In the example code below, we've changed the brightness based on dark or light mode, along with a blur. - - - -## Modal focus - -One of the features that Qwik UI provides is focus locking. This refers to keeping focus within the modal element. - - - -For example, when tabbing and the modal is opened, it will loop back to the first focusable element. - -> This is a default behavior that we provide out of the box, please [let us know](https://github.com/qwikifiers/qwik-ui/issues) if there are any additional use cases that require personalized focus lock behavior. - +Due to the modal's implementation using the dialog, the `::backdrop` pseudo element can be used as well. ## Accessibility @@ -157,3 +136,52 @@ For example, when tabbing and the modal is opened, it will loop back to the firs }, ]} /> + + +## API + +', + description: 'Toggle between showing or hiding the modal.', + }, + { + name: 'closeOnBackdropClick', + type: 'boolean', + description: 'A way to tell the modal to not hide when the ::backdrop is clicked.', + }, + { + name: 'bind:closeOnBackdropClick', + type: 'boolean', + description: 'A way to tell the modal to not hide when the ::backdrop is clicked.', + }, + { + name: '-Attributes', + type: 'QwikIntrinsicElements', + description: + 'A way to configure the modal with all native attributes the HTMLDialog defines.', + }, + { + name: 'onHide()$', + type: 'function', + info: '() => void', + description: + 'An event hook that gets notified whenever the modal gets hidden.', + }, + { + name: 'onShow()$', + type: 'function', + info: '() => void', + description: + 'An event hook that gets notified whenever the modal shows up.', + }, + ]} +/> From d0dc8782344284b73dc62cc98df831de4cfa5f41 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 13 Oct 2023 23:21:38 +0200 Subject: [PATCH 110/154] feat(modal): remove "show" & "bind:closeOnBackdropClick" --- .../src/components/modal/modal.tsx | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 0138aa7c1..971c9034c 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -25,40 +25,25 @@ import { export type ModalProps = Omit & { onShow$?: QRL<() => void>; onClose$?: QRL<() => void>; - show?: boolean; 'bind:show'?: Signal; closeOnBackdropClick?: boolean; - 'bind:closeOnBackdropClick'?: Signal; }; export const Modal = component$((props: ModalProps) => { const modalRefSig = useSignal(); const scrollbarWidth: WidthState = { width: null }; - const { 'bind:show': givenOpenSig, show: givenShow } = props; - const { - 'bind:closeOnBackdropClick': givenCloseOnBackdropClickSig, - closeOnBackdropClick: givenCloseOnBackdropClick, - } = props; + const { 'bind:show': givenOpenSig } = props; - const defaultOpenSig = useSignal(false); - const defaultCloseOnBackdropClickSig = useSignal(true); + const defaultShowSig = useSignal(false); + const showSig = givenOpenSig || defaultShowSig; - const showSig = givenOpenSig || defaultOpenSig; - const closeOnBackdropClickSig = - givenCloseOnBackdropClickSig || defaultCloseOnBackdropClickSig; + const closeOnBackdropClickSig = useSignal(true); - useTask$(async function syncShowProp({ track }) { - const showPropValue = track(() => givenShow); - - showSig.value = showPropValue || false; - }); - - useTask$(async function syncShowProp({ track }) { - const closeOnBackdropClickValue = track(() => givenCloseOnBackdropClick); - - closeOnBackdropClickSig.value = - closeOnBackdropClickValue === undefined ? true : closeOnBackdropClickValue; + useTask$(async function bindCloseOnBackdropClick({ track }) { + closeOnBackdropClickSig.value = track(() => + props.closeOnBackdropClick === undefined ? true : false, + ); }); useTask$(async function toggleModal({ track, cleanup }) { From afa0f02227f3518c06b72891f32e82cb4f162bf8 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 13 Oct 2023 23:26:41 +0200 Subject: [PATCH 111/154] docs(modal): remove "show" & "bind:closeOnBackdropClick" from API-Table --- .../routes/docs/headless/(components)/modal/index.mdx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index 144820188..11198249b 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -142,12 +142,6 @@ Due to the modal's implementation using the dialog, the `::backdrop` pseudo elem ', @@ -158,11 +152,6 @@ Due to the modal's implementation using the dialog, the `::backdrop` pseudo elem type: 'boolean', description: 'A way to tell the modal to not hide when the ::backdrop is clicked.', }, - { - name: 'bind:closeOnBackdropClick', - type: 'boolean', - description: 'A way to tell the modal to not hide when the ::backdrop is clicked.', - }, { name: '-Attributes', type: 'QwikIntrinsicElements', From b0d3cd756841afa46de4e80741910e9317e67524 Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Fri, 13 Oct 2023 20:17:22 -0500 Subject: [PATCH 112/154] docs(modal): added alertdialog & refactored, more docs --- .../docs/_components/api-table/api-table.tsx | 4 +- .../headless/(components)/modal/examples.tsx | 19 +++++ .../modal/examples/alert-dialog.tsx | 60 ++++++++++++++ .../modal/examples/backdrop-close.tsx | 80 ++++++++++++++++++ .../modal/examples/page-load-snip.tsx | 21 +++++ .../headless/(components)/modal/index.mdx | 82 ++++++++++++++++--- .../src/components/modal/modal.tsx | 4 +- 7 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/alert-dialog.tsx create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop-close.tsx create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/page-load-snip.tsx diff --git a/apps/website/src/routes/docs/_components/api-table/api-table.tsx b/apps/website/src/routes/docs/_components/api-table/api-table.tsx index 9b9e67d2d..250f3f037 100644 --- a/apps/website/src/routes/docs/_components/api-table/api-table.tsx +++ b/apps/website/src/routes/docs/_components/api-table/api-table.tsx @@ -28,12 +28,12 @@ export const APITable = component$(({ propDescriptors }: APITableProps) => { {propDescriptors?.map((propDescriptor) => { return ( - + {propDescriptor.name} - + {propDescriptor.type} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx index edaebf617..d62826474 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx @@ -14,7 +14,14 @@ import Backdrop from './examples/backdrop'; import backdropCssCode from './examples/backdrop.css?raw'; import backdropExampleCode from './examples/backdrop?raw'; +import BackdropClose from './examples/backdrop-close'; +import backdropCloseExampleCode from './examples/backdrop-close?raw'; + +import AlertDialog from './examples/alert-dialog'; +import alertDialogExampleCode from './examples/alert-dialog?raw'; + import buildingBlocksSnip from './examples/building-blocks-snip?raw'; +import pageLoadSnip from './examples/page-load-snip?raw'; import styles from './index.css?inline'; import { CodeExampleContainer } from '../../../_components/code-example/code-example-container'; @@ -42,6 +49,14 @@ export const examples: Record = { component: , code: backdropExampleCode, }, + backdropClose: { + component: , + code: backdropCloseExampleCode, + }, + alertDialog: { + component: , + code: alertDialogExampleCode, + }, }; export type ShowExampleProps = { @@ -68,3 +83,7 @@ export const BackdropCss = component$(() => ( export const BuildingBlocksSnip = component$(() => ( )); + +export const PageLoadSnip = component$(() => ( + +)); diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/alert-dialog.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/alert-dialog.tsx new file mode 100644 index 000000000..efb4575b3 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/alert-dialog.tsx @@ -0,0 +1,60 @@ +import { component$, useSignal, type QwikIntrinsicElements } from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; + +export default component$(() => { + const showSig = useSignal(false); + + return ( + <> + + + +

Deactive Account

+
+ +

Are you sure you want to deactivate your account?

+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop-close.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop-close.tsx new file mode 100644 index 000000000..9ca5a1a96 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/backdrop-close.tsx @@ -0,0 +1,80 @@ +import { type QwikIntrinsicElements, component$, useSignal } from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; + +export default component$(() => { + const showSig = useSignal(false); + + return ( + <> + + + +

Edit Profile

+

+ You can update your profile here. Hit the save button when finished. +

+
+ +
+ + +
+
+ + +
+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/page-load-snip.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/page-load-snip.tsx new file mode 100644 index 000000000..357af08c4 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/page-load-snip.tsx @@ -0,0 +1,21 @@ +import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; + +export default component$(() => { + const showSig = useSignal(false); + + useVisibleTask$( + () => { + showSig.value = true; + }, + { strategy: 'document-ready' }, + ); + + return ( + + + + + + ); +}); diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index 11198249b..0e245e078 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -13,7 +13,8 @@ import { LongExample, ShowExample, BackdropCss, - BuildingBlocksSnip + BuildingBlocksSnip, + PageLoadSnip } from './exports'; import testImage from '../../../../../../public/images/test-image.png' @@ -26,11 +27,11 @@ A window overlaid on either the primary window or another dialog window. Modal c -The modal makes use of HTML's `dialog` element, which has great browser support in [every major browser](https://caniuse.com/?search=dialog). +The modal makes use of HTML's `dialog` element, which has great support in [every major browser](https://caniuse.com/?search=dialog). > For non-modal UI elements, it is preferred to use [Qwik UI's popover component](../../../docs/headless/popover/) *(in progress)*. In the near future, Qwik's popover component can be applied to the most-semantically-relevant HTML element, including the `` element itself for non-modal dialogs. -Modals are used when an important choice needs to be made in the application. The rest of the content isn't interactive until a certain action has been performed. +Modals are used when an important choice needs to be made in the application. The rest of the content isn't interactive until a certain action has been performed.
[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/modal) @@ -42,9 +43,9 @@ Modals are used when an important choice needs to be made in the application. Th ## ✨ Features +- Follows the WAI ARIA Dialog & Alertdialog design patterns. - Managed focus order - Scroll locking -- Controlled or uncontrolled - Closes on escape - Toggle backdrop dismiss @@ -80,15 +81,15 @@ Modals are used when an important choice needs to be made in the application. Th The Qwik UI Modal component is built on top of the `dialog` element. To see additional capabilities of the dialog, take a look at the [offical MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog). -> To add dialog properties, it is the same as sticking the attribute, class, or API on the dialog element itself. +Our goal is to fill existing gaps, ensuring your modal is a11y compliant and enriched with HTML spec features, reducing reliance on Qwik UI maintainers. ### Attribute example -Above is an example of the `autofocus` attribute used to focus the 2nd input when the dialog is opened. +Above is an example of the `autofocus` attribute used to focus the 2nd input when the dialog is opened. -> When adding properties on ``, it is the same as sticking the attribute, class, or API on the dialog element itself. +> When adding properties on ``, it is the same as sticking the attribute, class, or API on the dialog element itself. ## Open or close the modal @@ -106,7 +107,7 @@ The above example sets the signal to true when the button with the text **Open M -In Chrome and Edge, a `top layer` attribute is shown in the dev tools. This means dialog content is placed above any other content, preventing any overflow or style issues. +In Chrome and Edge, a `top layer` UI button is shown in the dev tools. This means the element content is placed above any other content on the page, preventing any overflow or style issues. The top layer is used in Qwik UI, whenever a modal or popover is used. Popovers can be applied to any semantic element, modals in Qwik UI are tied to the `dialog` element. @@ -114,7 +115,66 @@ The top layer is used in Qwik UI, whenever a modal or popover is used. Popovers ## Backdrops -Due to the modal's implementation using the dialog, the `::backdrop` pseudo element can be used as well. +To add a modal backdrop, the `::backdrop` pseudo element can be utilized. [Backdrops](https://developer.mozilla.org/en-US/docs/Web/CSS/::backdrop) are right underneath top layer elements. + +By default, the `dialog` element comes with a subtle backdrop, below is a snippet customizing the backdrop background along with the [backdrop filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) css property. + + + + + +Styling a [backdrop in Tailwind](https://tailwindcss.com/docs/backdrop-blur) can be done using the `:backdrop` selector. In the example code below, we've changed the brightness based on dark or light mode, along with a blur. + + + +## Backdrop dismiss + +By default, modals can be dismissed by clicking on the backdrop. This can be changed with the `closeOnBackdropClick` prop. + + + +Above is an example of a modal that does not dismiss when the backdrop is clicked, where `closeOnBackdropClick` is set to false. + +## Alertdialog + +An alert dialog is a modal that interrupts user workflow to convey critical information and obtain a response. It's used for confirmations or error messages. + + + +By adding the `alert` prop to our component, we adhere to the [WAI ARIA specification](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/), enabling assistive technologies to distinguish alert dialogs for special handling. + +Alertdialogs do not allow the backdrop to be closed when clicked, regardless of whether the `closeOnBackdropClick` prop was set to true. + +## Modal focus + +One of the features that Qwik UI provides is focus locking. This refers to keeping focus within the modal component. + + + +For example, when tabbing and the modal is opened, it will loop back to the first focusable element. + +> This is a default behavior that we provide out of the box, please [let us know](https://github.com/qwikifiers/qwik-ui/issues) if there are any additional use cases that require personalized focus lock behavior. + +## Open on page load + +There might come a time where a modal is needed open as soon as the page loads. Unfortunately, to create modals a client-side API is currently needed regardless of implementation. + +### SSR dilemma + +Qwik is the first framework with an [SSR portal implementation](https://qwik.builder.io/docs/cookbook/portal/#solution). SSR Portal capabilities are unique to Qwik City, because the alternative frameworks require a client-side API for portal functionality, such as `document.appendChild`. + +The problem with rendering the modal in the server, is that we lose some critical behavior: + +- The content behind the dialog is not inert (non-modal) +- Focus and scroll lock customizations break +- Will not work with meta frameworks like Astro.js + +The current solution across framework ecosystems, is to open the Modal eagerly on the client to keep the proper functionality. + + + +> We believe that the dialog element will have native support for SSR Modals in the future, allowing you to gain this behavior when implemented. + ## Accessibility @@ -137,7 +197,6 @@ Due to the modal's implementation using the dialog, the `::backdrop` pseudo elem ]} /> - ## API -Attributes', - type: 'QwikIntrinsicElements', + type: `Qwik`, description: 'A way to configure the modal with all native attributes the HTMLDialog defines.', + info: `QwikIntrinsicElements['dialog']`, }, { name: 'onHide()$', diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 971c9034c..39ace4eb4 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -27,6 +27,7 @@ export type ModalProps = Omit & { onClose$?: QRL<() => void>; 'bind:show'?: Signal; closeOnBackdropClick?: boolean; + alert?: boolean; }; export const Modal = component$((props: ModalProps) => { @@ -76,7 +77,7 @@ export const Modal = component$((props: ModalProps) => { }); const closeOnBackdropClick$ = $((event: QwikMouseEvent) => { - if (!closeOnBackdropClickSig.value) { + if (props.alert === true || props.closeOnBackdropClick === false) { return; } @@ -87,6 +88,7 @@ export const Modal = component$((props: ModalProps) => { return ( Date: Mon, 16 Oct 2023 02:19:15 -0500 Subject: [PATCH 113/154] fix(docs): animation support, fixed scrollbar flickering across the board --- .../headless/(components)/modal/examples.tsx | 7 ++ .../(components)/modal/examples/animation.css | 14 +++ .../(components)/modal/examples/animation.tsx | 86 +++++++++++++++++++ .../headless/(components)/modal/index.mdx | 14 +-- .../src/components/modal/modal-behavior.ts | 40 ++++++++- .../src/components/modal/modal.css | 6 ++ .../src/components/modal/modal.tsx | 48 ++++++++--- packages/kit-headless/src/utils/clsq.ts | 62 +++++++++++++ 8 files changed, 254 insertions(+), 23 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx create mode 100644 packages/kit-headless/src/components/modal/modal.css create mode 100644 packages/kit-headless/src/utils/clsq.ts diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx index d62826474..67d82a6e2 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx @@ -20,6 +20,9 @@ import backdropCloseExampleCode from './examples/backdrop-close?raw'; import AlertDialog from './examples/alert-dialog'; import alertDialogExampleCode from './examples/alert-dialog?raw'; +import Animation from './examples/animation'; +import animationExampleCode from './examples/animation?raw'; + import buildingBlocksSnip from './examples/building-blocks-snip?raw'; import pageLoadSnip from './examples/page-load-snip?raw'; @@ -57,6 +60,10 @@ export const examples: Record = { component: , code: alertDialogExampleCode, }, + animation: { + component: , + code: animationExampleCode, + }, }; export type ShowExampleProps = { diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css new file mode 100644 index 000000000..42be2f0fd --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css @@ -0,0 +1,14 @@ +.my-modal { + opacity: 0; + position: fixed; + /* left: 200px; */ +} + +.my-modal[open] { + opacity: 1; + transition: opacity 400ms ease; +} + +.my-modal.closing { + opacity: 0; +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx new file mode 100644 index 000000000..2335577f5 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx @@ -0,0 +1,86 @@ +import { + component$, + useSignal, + type QwikIntrinsicElements, + useStyles$, +} from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; +import styles from './animation.css?inline'; + +export default component$(() => { + const showSig = useSignal(false); + useStyles$(styles); + + return ( + <> + + + +

Edit Profile

+

+ You can update your profile here. Hit the save button when finished. +

+
+ +
+ + +
+
+ + +
+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index 0e245e078..cc61d92a1 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -155,6 +155,10 @@ For example, when tabbing and the modal is opened, it will loop back to the firs > This is a default behavior that we provide out of the box, please [let us know](https://github.com/qwikifiers/qwik-ui/issues) if there are any additional use cases that require personalized focus lock behavior. +## Animations + + + ## Open on page load There might come a time where a modal is needed open as soon as the page loads. Unfortunately, to create modals a client-side API is currently needed regardless of implementation. @@ -173,12 +177,9 @@ The current solution across framework ecosystems, is to open the Modal eagerly o -> We believe that the dialog element will have native support for SSR Modals in the future, allowing you to gain this behavior when implemented. - - -## Accessibility +> We believe that the dialog element will have native support for SSR Modals in the future, gaining this behavior in the process. -### Keyboard interaction +## Keyboard interaction ', + type: 'Signal', + info: 'boolean', description: 'Toggle between showing or hiding the modal.', }, { diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 99e1b9519..186f8d09c 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -15,6 +15,11 @@ export function activateFocusTrap(focusTrap: FocusTrap | null) { } } +export function getFocusTrap(modal: HTMLDialogElement) { + const focusTrap = trapFocus(modal); + return focusTrap; +} + export function deactivateFocusTrap(focusTrap: FocusTrap | null) { focusTrap?.deactivate(); focusTrap = null; @@ -53,21 +58,48 @@ export function lockScroll() { window.document.body.style.overflow = 'hidden'; } -export function unlockScroll() { +export function unlockScroll(scrollbar: WidthElement) { window.document.body.style.overflow = ''; + + // cleanup the scroll padding + const currentPadding = parseInt(document.body.style.paddingRight); + if (scrollbar.width) { + document.body.style.paddingRight = `${currentPadding - scrollbar.width}px`; + } } export type WidthElement = { width: number | null; }; -export function preventScrollbarFlickering(scrollbar: WidthElement) { +export function adjustScrollbar(scrollbar: WidthElement) { if (scrollbar.width === null) { scrollbar.width = window.innerWidth - document.documentElement.clientWidth; } document.body.style.paddingRight = `${scrollbar.width}px`; } -function isTappable(modal: HTMLDialogElement) { - throw new Error('Function not implemented.'); + +// utility function to add support for animations & transitions +export function closing(modal: HTMLDialogElement, onClose$?: QRL<() => void>) { + if (!modal) { + return; + } + + modal.classList.add('closing'); + const { animationDuration, transitionDuration } = getComputedStyle(modal); + + if (animationDuration !== '0s') + modal.addEventListener('animationend', () => { + modal.classList.remove('closing'); + closeModal(modal, onClose$); + }); + else if (transitionDuration !== '0s') + modal.addEventListener('transitionend', () => { + modal.classList.remove('closing'); + closeModal(modal, onClose$); + }); + else { + closeModal(modal, onClose$); + } } diff --git a/packages/kit-headless/src/components/modal/modal.css b/packages/kit-headless/src/components/modal/modal.css new file mode 100644 index 000000000..002f65e76 --- /dev/null +++ b/packages/kit-headless/src/components/modal/modal.css @@ -0,0 +1,6 @@ +dialog { + animation: placeholder 0s; +} + +@keyframes placeholder { +} diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 39ace4eb4..25fa70313 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -8,20 +8,23 @@ import { Slot, useSignal, useTask$, + useStyles$, } from '@builder.io/qwik'; import { activateFocusTrap, - preventScrollbarFlickering as adjustScrollbar, - closeModal, deactivateFocusTrap, lockScroll, showModal, - trapFocus, - unlockScroll, + getFocusTrap, wasModalBackdropClicked, WidthElement as WidthState, + closing, + unlockScroll, + adjustScrollbar, } from './modal-behavior'; +import styles from './modal.css?inline'; + export type ModalProps = Omit & { onShow$?: QRL<() => void>; onClose$?: QRL<() => void>; @@ -31,6 +34,7 @@ export type ModalProps = Omit & { }; export const Modal = component$((props: ModalProps) => { + useStyles$(styles); const modalRefSig = useSignal(); const scrollbarWidth: WidthState = { width: null }; @@ -53,26 +57,46 @@ export const Modal = component$((props: ModalProps) => { if (!modal) return; - const focusTrap = trapFocus(modal); + const focusTrap = getFocusTrap(modal); + + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + + // fixes modal scrollbar flickering + modal.style.left = `${scrollbarWidth}px`; + + deactivateFocusTrap(focusTrap); + closing(modal, props.onClose$); + + showSig.value = false; + } + }; + + window.addEventListener('keydown', handleEscape); if (isOpen) { - adjustScrollbar(scrollbarWidth); + modal.style.left = `0px`; + showModal(modal, props.onShow$); + adjustScrollbar(scrollbarWidth); activateFocusTrap(focusTrap); lockScroll(); } else { - closeModal(modal, props.onClose$); + unlockScroll(scrollbarWidth); + closing(modal, props.onClose$); } cleanup(() => { deactivateFocusTrap(focusTrap); - unlockScroll(); - // cleanup the scroll padding - const currentPadding = parseInt(document.body.style.paddingRight); + // prevents closing animation scrollbar flickers (chrome & edge) if (scrollbarWidth.width) { - document.body.style.paddingRight = `${currentPadding - scrollbarWidth.width}px`; + const currLeft = parseInt(modal.style.left); + modal.style.left = `${scrollbarWidth.width - currLeft}px`; } + + window.removeEventListener('keydown', handleEscape); }); }); @@ -89,11 +113,9 @@ export const Modal = component$((props: ModalProps) => { return ( closeOnBackdropClick$(event)} - onClose$={() => (showSig.value = false)} > diff --git a/packages/kit-headless/src/utils/clsq.ts b/packages/kit-headless/src/utils/clsq.ts new file mode 100644 index 000000000..b9cda96d9 --- /dev/null +++ b/packages/kit-headless/src/utils/clsq.ts @@ -0,0 +1,62 @@ +/** + * Custom version of 'clsx' utility migrated to TypeScript. + */ + +export type ClassDictionary = Record; +export type ClassArray = ClassValue[]; +export type ClassValue = + | ClassArray + | ClassDictionary + | string + | number + | null + | boolean + | undefined; + +function toVal(mix: ClassValue) { + let str = ''; + + if (typeof mix === 'string' || typeof mix === 'number') { + str += mix; + } else if (typeof mix === 'object') { + if (Array.isArray(mix)) { + for (let k = 0; k < mix.length; k++) { + if (mix[k]) { + const y = toVal(mix[k]); + if (y) { + str && (str += ' '); + str += y; + } + } + } + } else { + for (const k in mix) { + if (mix[k]) { + str && (str += ' '); + str += k; + } + } + } + } + + return str; +} + +export function clsq(...inputs: ClassValue[]) { + let i = 0; + let tmp; + let str = ''; + while (i < inputs.length) { + tmp = inputs[i++]; + if (tmp) { + const x = toVal(tmp); + if (x) { + str && (str += ' '); + str += x; + } + } + } + return str; +} + +export default clsq; From 40cd056e9add2e08f4fe633bf14ced2dbc7b5d0a Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Tue, 17 Oct 2023 01:42:39 -0500 Subject: [PATCH 114/154] fix(modal): animations events are now properly cleaned up --- .../(components)/modal/examples/animation.css | 26 +++++++++++++---- .../(components)/modal/examples/animation.tsx | 4 ++- .../src/components/modal/modal-behavior.ts | 28 +++++++++++-------- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css index 42be2f0fd..eb43f8827 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css @@ -1,14 +1,28 @@ .my-modal { - opacity: 0; position: fixed; + animation: fadeIn 0.4s forwards; + /* left: 200px; */ } -.my-modal[open] { - opacity: 1; - transition: opacity 400ms ease; +.my-modal.closing { + animation: fadeOut 0.4s forwards; } -.my-modal.closing { - opacity: 0; +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } } diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx index 2335577f5..931cf3390 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.tsx @@ -14,7 +14,9 @@ export default component$(() => { return ( <>

Edit Profile

diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx index 8da86f0a7..ca8b05293 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/hero.tsx @@ -14,7 +14,8 @@ export default component$(() => {

Edit Profile

diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/inspect.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/inspect.tsx index fdbbef666..272381cfa 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/inspect.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/inspect.tsx @@ -10,7 +10,7 @@ export default component$(() => { onClick$={() => (showSig.value = true)} class="rounded-md border-2 border-slate-300 bg-slate-700 px-3 py-2 text-white" > - Inspect my modal! + Open Modal -The modal makes use of HTML's `dialog` element, which has great support in [every major browser](https://caniuse.com/?search=dialog). +The modal makes use of the HTML `dialog` element, which is supported in [every major browser](https://caniuse.com/?search=dialog). > For non-modal UI elements, it is preferred to use [Qwik UI's popover component](../../../docs/headless/popover/) *(in progress)*. In the near future, Qwik's popover component can be applied to the most-semantically-relevant HTML element, including the `` element itself for non-modal dialogs. @@ -48,6 +49,9 @@ Modals are used when an important choice needs to be made in the application. Th - Scroll locking - Closes on escape - Toggle backdrop dismiss +- Animation support +- Transition support +- Backdrop animations ## Building Blocks @@ -81,13 +85,13 @@ Modals are used when an important choice needs to be made in the application. Th The Qwik UI Modal component is built on top of the `dialog` element. To see additional capabilities of the dialog, take a look at the [offical MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog). -Our goal is to fill existing gaps, ensuring your modal is a11y compliant and enriched with HTML spec features, reducing reliance on Qwik UI maintainers. +Our goal is to fill existing gaps, ensuring the modal is a11y compliant and enriched with HTML spec features, reducing reliance on Qwik UI maintainers. ### Attribute example -Above is an example of the `autofocus` attribute used to focus the 2nd input when the dialog is opened. +Above is an example of the `autofocus` attribute, used to focus the 2nd input when the dialog is opened. > When adding properties on ``, it is the same as sticking the attribute, class, or API on the dialog element itself. @@ -95,21 +99,21 @@ Above is an example of the `autofocus` attribute used to focus the 2nd input whe To open or close the modal, we can use `bind:show`, a custom signal bind that leverages the [bind](https://qwik.builder.io/docs/components/rendering/#bind-attribute) syntax in Qwik. - + `showSig` is a signal that we pass into our Modal component to customize when the modal can be open or closed. -The above example sets the signal to true when the button with the text **Open Modal** is clicked, then closed whenever a button inside of the modal is clicked. +The example above opens the modal when the **Open Modal** button is clicked and closes it when a button inside the modal is clicked. -> Custom signal binds can be thought of as a remote control for a television. Just as you use a remote control to change channels, adjust volume, or turn the TV on or off, custom signal binds allow you to control the state of a component, such as opening or closing a modal, from a distance. +> Custom signal binds are like a remote control for components, controlling states like opening or closing a modal. -## The top layer +## The Top Layer In Chrome and Edge, a `top layer` UI button is shown in the dev tools. This means the element content is placed above any other content on the page, preventing any overflow or style issues. -The top layer is used in Qwik UI, whenever a modal or popover is used. Popovers can be applied to any semantic element, modals in Qwik UI are tied to the `dialog` element. +The top layer is used in Qwik UI whenever a modal or popover is used in supported browsers. Popovers can be applied to any semantic element, modals in Qwik UI are tied to the `dialog` element. > The previous implementation of modals required the use of [portals](https://qwik.builder.io/docs/cookbook/portal/#portal), the top layer pseudo class is a **native solution** to this problem. No more z-index wars! @@ -127,38 +131,60 @@ Styling a [backdrop in Tailwind](https://tailwindcss.com/docs/backdrop-blur) can -## Backdrop dismiss +## Dismissing Modals via Backdrop -By default, modals can be dismissed by clicking on the backdrop. This can be changed with the `closeOnBackdropClick` prop. +Modals in Qwik UI can be dismissed by default when the backdrop is clicked. However, this behavior can be modified using the `closeOnBackdropClick` property. -Above is an example of a modal that does not dismiss when the backdrop is clicked, where `closeOnBackdropClick` is set to false. +The example above demonstrates a modal where the `closeOnBackdropClick` property is set to false. As a result, clicking on the backdrop does not dismiss the modal. ## Alertdialog -An alert dialog is a modal that interrupts user workflow to convey critical information and obtain a response. It's used for confirmations or error messages. +An alert dialog is a modal that interrupts user workflow to convey critical information and obtain a response. It's typically used for confirmations, error messages, or destructive actions. -By adding the `alert` prop to our component, we adhere to the [WAI ARIA specification](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/), enabling assistive technologies to distinguish alert dialogs for special handling. +By adding the `alert` prop to our component, we adhere to the [WAI ARIA Alertdialog specification](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/), enabling assistive technologies to distinguish alert dialogs for special handling. Alertdialogs do not allow the backdrop to be closed when clicked, regardless of whether the `closeOnBackdropClick` prop was set to true. -## Modal focus +## Focus Locking in Modals -One of the features that Qwik UI provides is focus locking. This refers to keeping focus within the modal component. +Qwik UI incorporates a feature known as focus locking, which confines the focus within the modal component when it is open. - + -For example, when tabbing and the modal is opened, it will loop back to the first focusable element. +This means that if a user is navigating through the modal using the Tab key, reaching the last focusable element and pressing Tab again will cycle the focus back to the first focusable element within the modal. -> This is a default behavior that we provide out of the box, please [let us know](https://github.com/qwikifiers/qwik-ui/issues) if there are any additional use cases that require personalized focus lock behavior. +> Focus locking behavior is provided by default in Qwik UI. If you encounter any use cases that require customized focus lock behavior, please [submit an issue](https://github.com/qwikifiers/qwik-ui/issues) on our GitHub repository. ## Animations +Animating things to display none has historically been a significant challenge on the web. This is because display none is a `discrete` property, and is **unanimatable**. + +> There is currently efforts to solve this problem. [New CSS properties](https://developer.chrome.com/blog/entry-exit-animations/) have been introduced, but currently do not provide good enough browser support. + +### Our current approach + +Qwik UI automatically detects any `animation` or `transition` declarations under the hood and waits for them to finish before closing the modal. If there is no animation, then it will close normally. + +### Adding an animation + +To add an animation, use the `modal-showing` and `modal-closing` css classes. Below is a snippet of the animation example above. + + + +### Backdrop animations + +Backdrop animations have also made significant progress in the last year, with support provided in over half of the major browsers, and close to 70% of users. + +To add a backdrop animation, make sure to use the `::backdrop` pseudo selector to the end of the `modal-closing` or `modal-opening` classes. + +> Firefox currently does not support backdrop animations. The fallback for browsers that do not support animated backdrops is the same as a non-animated backdrop. + ## Open on page load There might come a time where a modal is needed open as soon as the page loads. Unfortunately, to create modals a client-side API is currently needed regardless of implementation. @@ -234,5 +260,17 @@ The current solution across framework ecosystems, is to open the Modal eagerly o description: 'An event hook that gets notified whenever the modal shows up.', }, + { + name: '.modal-showing', + type: 'CSS', + description: + 'A CSS class used for entry animations.', + }, + { + name: '.modal-closing', + type: 'CSS', + description: + 'A CSS class used for exit animations.', + }, ]} /> diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 58288a893..815019684 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -101,11 +101,12 @@ export function unlockScroll(scrollbar: WidthElement) { * TODO: Why??? * */ -export function adjustScrollbar(scrollbar: WidthElement) { +export function adjustScrollbar(scrollbar: WidthElement, modal: HTMLDialogElement) { if (scrollbar.width === null) { scrollbar.width = window.innerWidth - document.documentElement.clientWidth; } + modal.style.left = `0px`; document.body.style.paddingRight = `${scrollbar.width}px`; } diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 707981c67..df70c72ac 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -63,9 +63,6 @@ export const Modal = component$((props: ModalProps) => { if (e.key === 'Escape') { e.preventDefault(); - // fixes modal scrollbar flickering - modal.style.left = `${scrollbarWidth}px`; - showSig.value = false; } }; @@ -73,9 +70,8 @@ export const Modal = component$((props: ModalProps) => { window.addEventListener('keydown', handleEscape); if (isOpen) { - modal.style.left = `0px`; showModal(modal, props.onShow$); - adjustScrollbar(scrollbarWidth); + adjustScrollbar(scrollbarWidth, modal); activateFocusTrap(focusTrap); lockScroll(); } else { From 75ba5dfcd4f25fca297868abadef4f8de96acdd6 Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Thu, 19 Oct 2023 10:59:58 -0500 Subject: [PATCH 132/154] docs(modal): remove unused examples --- .../src/routes/docs/headless/(components)/modal/index.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index 1f07d3652..a90ffe597 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -9,8 +9,6 @@ import { AnatomyTable, StatusBanner, statusByComponent, - ShortExample, - LongExample, ShowExample, BackdropCss, BuildingBlocksSnip, From 3e1bd4af592731eb41fcfa82f768a34f3a72cdb0 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 16:52:27 +0200 Subject: [PATCH 133/154] refactor(modal): make binding of closeOnBackdropClick more expressive --- packages/kit-headless/src/components/modal/modal.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index df70c72ac..48c0b490d 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -46,9 +46,13 @@ export const Modal = component$((props: ModalProps) => { const closeOnBackdropClickSig = useSignal(true); useTask$(async function bindCloseOnBackdropClick({ track }) { - closeOnBackdropClickSig.value = track(() => - props.closeOnBackdropClick === undefined ? true : false, - ); + const closeOnBackdropClick = track(() => props.closeOnBackdropClick); + + if (closeOnBackdropClick === undefined || closeOnBackdropClick === true) { + closeOnBackdropClickSig.value = true; + } else { + closeOnBackdropClickSig.value = false; + } }); useTask$(async function toggleModal({ track, cleanup }) { From e12cfbb61ae2d61771ff3407d77acd0847e3ec79 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 17:10:42 +0200 Subject: [PATCH 134/154] refactor(modal): extract ESC-Key overriding --- .../src/components/modal/modal-behavior.ts | 10 ++++++++++ .../kit-headless/src/components/modal/modal.tsx | 16 ++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 815019684..bf4671028 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -110,6 +110,16 @@ export function adjustScrollbar(scrollbar: WidthElement, modal: HTMLDialogElemen document.body.style.paddingRight = `${scrollbar.width}px`; } +export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) { + return function handleKeydown(e: KeyboardEvent) { + if (e.key === 'Escape') { + e.preventDefault(); + + continuation(); + } + }; +} + /* * Listens for animation/transition events in order to * remove Animation-CSS-Classes after animation/transition ended. diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 48c0b490d..fce57eb91 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -17,6 +17,7 @@ import { closing, deactivateFocusTrap, lockScroll, + overrideNativeDialogEscapeBehaviorWith, showModal, trapFocus, unlockScroll, @@ -62,16 +63,11 @@ export const Modal = component$((props: ModalProps) => { if (!modal) return; const focusTrap = trapFocus(modal); + const escapeKeydownHandler = overrideNativeDialogEscapeBehaviorWith( + () => (showSig.value = false), + ); - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - - showSig.value = false; - } - }; - - window.addEventListener('keydown', handleEscape); + window.addEventListener('keydown', escapeKeydownHandler); if (isOpen) { showModal(modal, props.onShow$); @@ -92,7 +88,7 @@ export const Modal = component$((props: ModalProps) => { modal.style.left = `${scrollbarWidth.width - currLeft}px`; } - window.removeEventListener('keydown', handleEscape); + window.removeEventListener('keydown', escapeKeydownHandler); }); }); From 542699b612b3178af4cdab5ed4502184c5228759 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 17:13:35 +0200 Subject: [PATCH 135/154] refactor(modal): improve property name --- packages/kit-headless/src/components/modal/modal.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index fce57eb91..db328087c 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -37,7 +37,7 @@ export type ModalProps = Omit & { export const Modal = component$((props: ModalProps) => { useStyles$(styles); const modalRefSig = useSignal(); - const scrollbarWidth: WidthState = { width: null }; + const scrollbarWidthState: WidthState = { width: null }; const { 'bind:show': givenOpenSig } = props; @@ -63,6 +63,7 @@ export const Modal = component$((props: ModalProps) => { if (!modal) return; const focusTrap = trapFocus(modal); + const escapeKeydownHandler = overrideNativeDialogEscapeBehaviorWith( () => (showSig.value = false), ); @@ -71,11 +72,11 @@ export const Modal = component$((props: ModalProps) => { if (isOpen) { showModal(modal, props.onShow$); - adjustScrollbar(scrollbarWidth, modal); + adjustScrollbar(scrollbarWidthState, modal); activateFocusTrap(focusTrap); lockScroll(); } else { - unlockScroll(scrollbarWidth); + unlockScroll(scrollbarWidthState); closing(modal, props.onClose$); } @@ -83,9 +84,9 @@ export const Modal = component$((props: ModalProps) => { deactivateFocusTrap(focusTrap); // prevents closing animation scrollbar flickers (chrome & edge) - if (scrollbarWidth.width) { + if (scrollbarWidthState.width) { const currLeft = parseInt(modal.style.left); - modal.style.left = `${scrollbarWidth.width - currLeft}px`; + modal.style.left = `${scrollbarWidthState.width - currLeft}px`; } window.removeEventListener('keydown', escapeKeydownHandler); From c2004cf3d4fcd165ca60d282e9e2d32b62d04137 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 17:21:54 +0200 Subject: [PATCH 136/154] refactor(modal): extract modal position fix when scrollbar reappears --- .../src/components/modal/modal-behavior.ts | 21 +++++++++++++++++++ .../src/components/modal/modal.tsx | 2 ++ 2 files changed, 23 insertions(+) diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index bf4671028..52130663a 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -120,6 +120,27 @@ export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) }; } +/** + * When the Modal is closed we are enabling scrolling again. + * This means the scrollbar will reappear in the browser. + * The scrollbar has a width and causes the Modal to reposition. + * + * That's why we take the scrollbar-width into account so that the + * Modal remains in the same position as before. + */ +export function keepModalInPlaceWhileScrollbarReappears( + scrollbar: WidthElement, + modal?: HTMLDialogElement, +) { + if (!modal) return; + + if (scrollbar.width) { + const modalLeft = parseInt(modal.style.left); + + modal.style.left = `${scrollbar.width - modalLeft}px`; + } +} + /* * Listens for animation/transition events in order to * remove Animation-CSS-Classes after animation/transition ended. diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index db328087c..946aca605 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -86,6 +86,8 @@ export const Modal = component$((props: ModalProps) => { // prevents closing animation scrollbar flickers (chrome & edge) if (scrollbarWidthState.width) { const currLeft = parseInt(modal.style.left); + console.log(scrollbarWidthState, currLeft); + modal.style.left = `${scrollbarWidthState.width - currLeft}px`; } From cee002360f819bad586749303cacf3ca35db3798 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 17:22:33 +0200 Subject: [PATCH 137/154] refactor(modal): remove props binding since it was not used --- .../src/components/modal/modal.tsx | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 946aca605..1535019b4 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -16,6 +16,7 @@ import { adjustScrollbar, closing, deactivateFocusTrap, + keepModalInPlaceWhileScrollbarReappears, lockScroll, overrideNativeDialogEscapeBehaviorWith, showModal, @@ -37,25 +38,13 @@ export type ModalProps = Omit & { export const Modal = component$((props: ModalProps) => { useStyles$(styles); const modalRefSig = useSignal(); - const scrollbarWidthState: WidthState = { width: null }; + const scrollbar: WidthState = { width: null }; const { 'bind:show': givenOpenSig } = props; const defaultShowSig = useSignal(false); const showSig = givenOpenSig || defaultShowSig; - const closeOnBackdropClickSig = useSignal(true); - - useTask$(async function bindCloseOnBackdropClick({ track }) { - const closeOnBackdropClick = track(() => props.closeOnBackdropClick); - - if (closeOnBackdropClick === undefined || closeOnBackdropClick === true) { - closeOnBackdropClickSig.value = true; - } else { - closeOnBackdropClickSig.value = false; - } - }); - useTask$(async function toggleModal({ track, cleanup }) { const isOpen = track(() => showSig.value); const modal = modalRefSig.value; @@ -72,25 +61,17 @@ export const Modal = component$((props: ModalProps) => { if (isOpen) { showModal(modal, props.onShow$); - adjustScrollbar(scrollbarWidthState, modal); + adjustScrollbar(scrollbar, modal); activateFocusTrap(focusTrap); lockScroll(); } else { - unlockScroll(scrollbarWidthState); + unlockScroll(scrollbar); closing(modal, props.onClose$); } cleanup(() => { deactivateFocusTrap(focusTrap); - - // prevents closing animation scrollbar flickers (chrome & edge) - if (scrollbarWidthState.width) { - const currLeft = parseInt(modal.style.left); - console.log(scrollbarWidthState, currLeft); - - modal.style.left = `${scrollbarWidthState.width - currLeft}px`; - } - + keepModalInPlaceWhileScrollbarReappears(scrollbar, modalRefSig.value); window.removeEventListener('keydown', escapeKeydownHandler); }); }); From 0da3e6b84f1738cb6e1804df8f2205c041ab4995 Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Fri, 20 Oct 2023 14:00:50 -0500 Subject: [PATCH 138/154] feat(modal): transition support, fixed type errors in spec --- .../headless/(components)/modal/examples.tsx | 7 ++ .../modal/examples/transition.tsx | 105 ++++++++++++++++++ .../headless/(components)/modal/index.mdx | 12 +- .../src/components/modal/modal-behavior.ts | 1 - .../src/components/modal/modal.spec.tsx | 2 +- 5 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx index 6fd2c6163..b04f1aa13 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx @@ -23,6 +23,9 @@ import alertDialogExampleCode from './examples/alert-dialog?raw'; import Animation from './examples/animation'; import animationExampleCode from './examples/animation?raw'; +import Transition from './examples/transition'; +import transitionExampleCode from './examples/transition?raw'; + import buildingBlocksSnip from './examples/building-blocks-snip?raw'; import pageLoadSnip from './examples/page-load-snip?raw'; import animationSnip from './examples/animation-snip.css?raw'; @@ -65,6 +68,10 @@ export const examples: Record = { component: , code: animationExampleCode, }, + transition: { + component: , + code: transitionExampleCode, + }, }; export type ShowExampleProps = { diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx new file mode 100644 index 000000000..4608bf2e0 --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx @@ -0,0 +1,105 @@ +import { + component$, + useSignal, + type QwikIntrinsicElements, + useStyles$, +} from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; +// import styles from './animation.css?inline'; + +export default component$(() => { + const showSig = useSignal(false); + useStyles$(` + .my-transition::backdrop { + background: rgba(0,0,0,0.5); + } + + .my-transition, .my-transition::backdrop { + opacity: 0; + transition: opacity 500ms ease; + } + + .my-transition.modal-showing, .my-transition.modal-showing::backdrop { + opacity: 1; + } + + .my-transition.modal-closing, .my-transition.modal-closing::backdrop { + opacity: 0; + } + `); + + return ( + <> + + + +

Edit Profile

+
+ +

+ You can update your profile here. Hit the save button when finished. +

+
+ + +
+
+ + +
+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index a90ffe597..24fb9c6eb 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -159,8 +159,6 @@ This means that if a user is navigating through the modal using the Tab key, rea ## Animations - - Animating things to display none has historically been a significant challenge on the web. This is because display none is a `discrete` property, and is **unanimatable**. > There is currently efforts to solve this problem. [New CSS properties](https://developer.chrome.com/blog/entry-exit-animations/) have been introduced, but currently do not provide good enough browser support. @@ -169,9 +167,17 @@ Animating things to display none has historically been a significant challenge o Qwik UI automatically detects any `animation` or `transition` declarations under the hood and waits for them to finish before closing the modal. If there is no animation, then it will close normally. +### Adding a transition + + + +To add an transition, use the `modal-showing` and `modal-closing` css classes. Above is a snippet where we transition both the modal and backdrop's opacity. + ### Adding an animation -To add an animation, use the `modal-showing` and `modal-closing` css classes. Below is a snippet of the animation example above. + + +To add an animation, it's the same as with transitions, using the `modal-showing` and `modal-closing` css classes. Below is a snippet of the animation example above. diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 52130663a..de102e912 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -195,7 +195,6 @@ export function opening(modal: HTMLDialogElement) { }; const runTransitionEnd = () => { - modal.classList.remove('modal-showing'); modal.removeEventListener('transitionend', runTransitionEnd); }; diff --git a/packages/kit-headless/src/components/modal/modal.spec.tsx b/packages/kit-headless/src/components/modal/modal.spec.tsx index 63f5ede38..cc412bf35 100644 --- a/packages/kit-headless/src/components/modal/modal.spec.tsx +++ b/packages/kit-headless/src/components/modal/modal.spec.tsx @@ -8,7 +8,7 @@ import { ModalHeader } from './modal-header'; * SUT - System under test * Reference: https://en.wikipedia.org/wiki/System_under_test */ -const Sut = component$((props?: ModalProps) => { +const Sut = component$((props: ModalProps) => { const showSig = useSignal(false); useStyles$(` From 99ae6284ce69c0687a6321817e121cd5cab3395f Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 20:49:37 +0200 Subject: [PATCH 139/154] feat(modal): require bind:show --- .../(components)/modal/examples/building-blocks-snip.tsx | 6 ++++-- .../kit-headless/src/components/modal/modal-behavior.ts | 8 ++++---- packages/kit-headless/src/components/modal/modal.tsx | 9 +++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx index afcaebcae..c8f46fa03 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/building-blocks-snip.tsx @@ -1,9 +1,11 @@ -import { component$ } from '@builder.io/qwik'; +import { component$, useSignal } from '@builder.io/qwik'; import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; export default component$(() => { + const showSig = useSignal(false); + return ( - + diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index de102e912..ee1326369 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -78,7 +78,7 @@ export function lockScroll() { window.document.body.style.overflow = 'hidden'; } -export type WidthElement = { +export type WidthState = { width: number | null; }; @@ -86,7 +86,7 @@ export type WidthElement = { * Unlocks scrolling of the document. * Adjusts padding of the given scrollbar. */ -export function unlockScroll(scrollbar: WidthElement) { +export function unlockScroll(scrollbar: WidthState) { window.document.body.style.overflow = ''; const currentPadding = parseInt(document.body.style.paddingRight); @@ -101,7 +101,7 @@ export function unlockScroll(scrollbar: WidthElement) { * TODO: Why??? * */ -export function adjustScrollbar(scrollbar: WidthElement, modal: HTMLDialogElement) { +export function adjustScrollbar(scrollbar: WidthState, modal: HTMLDialogElement) { if (scrollbar.width === null) { scrollbar.width = window.innerWidth - document.documentElement.clientWidth; } @@ -129,7 +129,7 @@ export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) * Modal remains in the same position as before. */ export function keepModalInPlaceWhileScrollbarReappears( - scrollbar: WidthElement, + scrollbar: WidthState, modal?: HTMLDialogElement, ) { if (!modal) return; diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 1535019b4..39453df90 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -11,7 +11,7 @@ import { useTask$, } from '@builder.io/qwik'; import { - WidthElement as WidthState, + WidthState, activateFocusTrap, adjustScrollbar, closing, @@ -30,7 +30,7 @@ import styles from './modal.css?inline'; export type ModalProps = Omit & { onShow$?: QRL<() => void>; onClose$?: QRL<() => void>; - 'bind:show'?: Signal; + 'bind:show': Signal; closeOnBackdropClick?: boolean; alert?: boolean; }; @@ -40,10 +40,7 @@ export const Modal = component$((props: ModalProps) => { const modalRefSig = useSignal(); const scrollbar: WidthState = { width: null }; - const { 'bind:show': givenOpenSig } = props; - - const defaultShowSig = useSignal(false); - const showSig = givenOpenSig || defaultShowSig; + const { 'bind:show': showSig } = props; useTask$(async function toggleModal({ track, cleanup }) { const isOpen = track(() => showSig.value); From cd876486f35fb82d2c4d5f91ffc7c446b483d19a Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 21:04:29 +0200 Subject: [PATCH 140/154] test(modal): ensure opening button is focused after closing --- packages/kit-headless/src/components/modal/modal.spec.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal.spec.tsx b/packages/kit-headless/src/components/modal/modal.spec.tsx index cc412bf35..2baacc187 100644 --- a/packages/kit-headless/src/components/modal/modal.spec.tsx +++ b/packages/kit-headless/src/components/modal/modal.spec.tsx @@ -111,9 +111,10 @@ describe('Modal', () => { }); it(`GIVEN a Modal - WHEN opening it + WHEN opening it with a button AND clicking the backdrop - THEN it is not visible any more`, () => { + THEN it is not visible any more + THEN it focuses the opening button`, () => { cy.mount(); cy.get('[data-test=modal-trigger]').click(); @@ -121,6 +122,8 @@ describe('Modal', () => { cy.get('body').click('top'); cy.get('dialog').should('not.be.visible'); + + cy.get('[data-test=modal-trigger]').should('be.focused'); }); it(`Given a Modal From 1108fa8e5ddd8e835991d724860b9f5f70d4df7b Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Fri, 20 Oct 2023 14:09:43 -0500 Subject: [PATCH 141/154] docs(modal): note about invokers in the future --- .../src/routes/docs/headless/(components)/modal/index.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index 24fb9c6eb..2752ea0d5 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -103,7 +103,9 @@ To open or close the modal, we can use `bind:show`, a custom signal bind that le The example above opens the modal when the **Open Modal** button is clicked and closes it when a button inside the modal is clicked. -> Custom signal binds are like a remote control for components, controlling states like opening or closing a modal. +> In the future, we may not need to manage state for the opening and closing of dialogs and other UI elements. Here's Open UI's [invoker proposal](https://open-ui.org/components/invokers.explainer/). + +Custom signal binds are like a remote control for components, controlling states like opening or closing a modal. ## The Top Layer From a8bc5b9f4a92a05574b5543941dd59da45c9e932 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 21:41:14 +0200 Subject: [PATCH 142/154] refactor(modal): register animated/transitioned once --- .../kit-headless/src/components/modal/modal-behavior.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index ee1326369..4ea9bebbf 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -157,19 +157,17 @@ export function closing(modal: HTMLDialogElement, onClose$?: QRL<() => void>) { const runAnimationEnd = () => { modal.classList.remove('modal-closing'); closeModal(modal, onClose$); - modal.removeEventListener('animationend', runAnimationEnd); }; const runTransitionEnd = () => { modal.classList.remove('modal-closing'); closeModal(modal, onClose$); - modal.removeEventListener('transitionend', runTransitionEnd); }; if (animationDuration !== '0s') { - modal.addEventListener('animationend', runAnimationEnd); + modal.addEventListener('animationend', runAnimationEnd, { once: true }); } else if (transitionDuration !== '0s') { - modal.addEventListener('transitionend', runTransitionEnd); + modal.addEventListener('transitionend', runTransitionEnd, { once: true }); } else { modal.classList.remove('modal-closing'); closeModal(modal, onClose$); From 99b30becf493b4ae6bb9f92d1fc3e85e64330ac7 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 21:41:37 +0200 Subject: [PATCH 143/154] refactor(modal): simplify modal opening --- .../src/components/modal/modal-behavior.ts | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 4ea9bebbf..af575d555 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -175,32 +175,10 @@ export function closing(modal: HTMLDialogElement, onClose$?: QRL<() => void>) { } /* - * Listens for animation/transition events in order to - * remove Animation-CSS-Classes after animation/transition ended. + * Adds CSS-Class to support modal-opening-animation */ export function opening(modal: HTMLDialogElement) { - if (!modal) { - return; - } + if (!modal) return; modal.classList.add('modal-showing'); - - const { animationDuration, transitionDuration } = getComputedStyle(modal); - - const runAnimationEnd = () => { - modal.classList.remove('modal-showing'); - modal.removeEventListener('animationend', runAnimationEnd); - }; - - const runTransitionEnd = () => { - modal.removeEventListener('transitionend', runTransitionEnd); - }; - - if (animationDuration !== '0s') { - modal.addEventListener('animationend', runAnimationEnd); - } else if (transitionDuration !== '0s') { - modal.addEventListener('transitionend', runTransitionEnd); - } else { - modal.classList.remove('modal-showing'); - } } From f6ce31822a26eb768503233873c89325a4884666 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 21:56:00 +0200 Subject: [PATCH 144/154] test(modal): expecting .modal-showing to be applied after modal has been opened --- packages/kit-headless/src/components/modal/modal.spec.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit-headless/src/components/modal/modal.spec.tsx b/packages/kit-headless/src/components/modal/modal.spec.tsx index 2baacc187..2904202c4 100644 --- a/packages/kit-headless/src/components/modal/modal.spec.tsx +++ b/packages/kit-headless/src/components/modal/modal.spec.tsx @@ -238,7 +238,6 @@ describe('Modal', () => { cy.get('dialog').should('have.class', 'modal-showing'); cy.get('[data-test=modal-header]').should('be.visible'); - cy.get('dialog').should('not.have.class', 'modal-showing'); cy.realPress('Escape'); From cc6516aab763562c39690e5d99a1cad61a574490 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 21:56:56 +0200 Subject: [PATCH 145/154] refactor(modal): restructure helpers adding animation support --- .../src/components/modal/modal-behavior.ts | 59 +++++++++---------- .../src/components/modal/modal.tsx | 5 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index af575d555..09c84e42a 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -1,6 +1,10 @@ import { QRL, QwikMouseEvent } from '@builder.io/qwik'; import { FocusTrap, createFocusTrap } from 'focus-trap'; +export type WidthState = { + width: number | null; +}; + /** * Traps the focus of the given Modal * @returns FocusTrap @@ -29,23 +33,22 @@ export function deactivateFocusTrap(focusTrap: FocusTrap | null) { /** * Shows the given Modal. - * Applies CSS-Class to animate the modal-showing. + * Applies a CSS-Class to animate the modal-showing. * Calls the given callback that is executed after the Modal has been opened. */ export async function showModal(modal: HTMLDialogElement, onShow$?: QRL<() => void>) { modal.showModal(); - opening(modal); + supportShowAnimation(modal); await onShow$?.(); } /** * Closes the given Modal. - * Applies CSS-Class to animate the Modal-closing. + * Applies a CSS-Class to animate the Modal-closing. * Calls the given callback that is executed after the Modal has been closed. */ export async function closeModal(modal: HTMLDialogElement, onClose$?: QRL<() => void>) { - modal.close(); - modal.classList.remove('modal-showing'); + supportClosingAnimation(modal, () => modal.close()); await onClose$?.(); } @@ -78,10 +81,6 @@ export function lockScroll() { window.document.body.style.overflow = 'hidden'; } -export type WidthState = { - width: number | null; -}; - /** * Unlocks scrolling of the document. * Adjusts padding of the given scrollbar. @@ -96,10 +95,12 @@ export function unlockScroll(scrollbar: WidthState) { } /** + * When the Modal is opened we are disabling scrolling. + * This means the scrollbar will vanish. + * The scrollbar has a width and causes the Modal to reposition. * - * Adjusts scrollbar padding - * TODO: Why??? - * + * That's why we take the scrollbar-width into account so that the + * Modal does not jump to the right. */ export function adjustScrollbar(scrollbar: WidthState, modal: HTMLDialogElement) { if (scrollbar.width === null) { @@ -121,7 +122,7 @@ export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) } /** - * When the Modal is closed we are enabling scrolling again. + * When the Modal is closed we are enabling scrolling. * This means the scrollbar will reappear in the browser. * The scrollbar has a width and causes the Modal to reposition. * @@ -141,27 +142,34 @@ export function keepModalInPlaceWhileScrollbarReappears( } } +/* + * Adds CSS-Class to support modal-opening-animation + */ +export function supportShowAnimation(modal: HTMLDialogElement) { + modal.classList.add('modal-showing'); +} + /* * Listens for animation/transition events in order to * remove Animation-CSS-Classes after animation/transition ended. */ -export function closing(modal: HTMLDialogElement, onClose$?: QRL<() => void>) { - if (!modal) { - return; - } - +export function supportClosingAnimation( + modal: HTMLDialogElement, + afterAnimate: () => void, +) { + modal.classList.remove('modal-showing'); modal.classList.add('modal-closing'); const { animationDuration, transitionDuration } = getComputedStyle(modal); const runAnimationEnd = () => { modal.classList.remove('modal-closing'); - closeModal(modal, onClose$); + afterAnimate(); }; const runTransitionEnd = () => { modal.classList.remove('modal-closing'); - closeModal(modal, onClose$); + afterAnimate(); }; if (animationDuration !== '0s') { @@ -170,15 +178,6 @@ export function closing(modal: HTMLDialogElement, onClose$?: QRL<() => void>) { modal.addEventListener('transitionend', runTransitionEnd, { once: true }); } else { modal.classList.remove('modal-closing'); - closeModal(modal, onClose$); + afterAnimate(); } } - -/* - * Adds CSS-Class to support modal-opening-animation - */ -export function opening(modal: HTMLDialogElement) { - if (!modal) return; - - modal.classList.add('modal-showing'); -} diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 39453df90..897594cab 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -14,7 +14,7 @@ import { WidthState, activateFocusTrap, adjustScrollbar, - closing, + closeModal, deactivateFocusTrap, keepModalInPlaceWhileScrollbarReappears, lockScroll, @@ -63,7 +63,8 @@ export const Modal = component$((props: ModalProps) => { lockScroll(); } else { unlockScroll(scrollbar); - closing(modal, props.onClose$); + closeModal(modal); + // animateClosing(modal, props.onClose$); } cleanup(() => { From 8353aeb9dd1b63a9432e01ef8d1b2188181ad429 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 21:59:24 +0200 Subject: [PATCH 146/154] refactor(modal): call onShow$ & onClose$ in Modal directly --- .../kit-headless/src/components/modal/modal-behavior.ts | 8 +++----- packages/kit-headless/src/components/modal/modal.tsx | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 09c84e42a..22e398a40 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -1,4 +1,4 @@ -import { QRL, QwikMouseEvent } from '@builder.io/qwik'; +import { QwikMouseEvent } from '@builder.io/qwik'; import { FocusTrap, createFocusTrap } from 'focus-trap'; export type WidthState = { @@ -36,10 +36,9 @@ export function deactivateFocusTrap(focusTrap: FocusTrap | null) { * Applies a CSS-Class to animate the modal-showing. * Calls the given callback that is executed after the Modal has been opened. */ -export async function showModal(modal: HTMLDialogElement, onShow$?: QRL<() => void>) { +export async function showModal(modal: HTMLDialogElement) { modal.showModal(); supportShowAnimation(modal); - await onShow$?.(); } /** @@ -47,9 +46,8 @@ export async function showModal(modal: HTMLDialogElement, onShow$?: QRL<() => vo * Applies a CSS-Class to animate the Modal-closing. * Calls the given callback that is executed after the Modal has been closed. */ -export async function closeModal(modal: HTMLDialogElement, onClose$?: QRL<() => void>) { +export async function closeModal(modal: HTMLDialogElement) { supportClosingAnimation(modal, () => modal.close()); - await onClose$?.(); } /** diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 897594cab..20f24f867 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -57,14 +57,15 @@ export const Modal = component$((props: ModalProps) => { window.addEventListener('keydown', escapeKeydownHandler); if (isOpen) { - showModal(modal, props.onShow$); + showModal(modal); + props.onShow$?.(); adjustScrollbar(scrollbar, modal); activateFocusTrap(focusTrap); lockScroll(); } else { unlockScroll(scrollbar); closeModal(modal); - // animateClosing(modal, props.onClose$); + props.onClose$?.(); } cleanup(() => { From 4c3da818a96781b5a7a948a9ed340d1962ebfe86 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 22:00:48 +0200 Subject: [PATCH 147/154] refactor(modal): simplify ESC-handler by using { once: true } --- packages/kit-headless/src/components/modal/modal.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 20f24f867..32b1fd1c5 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -50,12 +50,12 @@ export const Modal = component$((props: ModalProps) => { const focusTrap = trapFocus(modal); - const escapeKeydownHandler = overrideNativeDialogEscapeBehaviorWith( - () => (showSig.value = false), + window.addEventListener( + 'keydown', + overrideNativeDialogEscapeBehaviorWith(() => (showSig.value = false)), + { once: true }, ); - window.addEventListener('keydown', escapeKeydownHandler); - if (isOpen) { showModal(modal); props.onShow$?.(); @@ -71,7 +71,6 @@ export const Modal = component$((props: ModalProps) => { cleanup(() => { deactivateFocusTrap(focusTrap); keepModalInPlaceWhileScrollbarReappears(scrollbar, modalRefSig.value); - window.removeEventListener('keydown', escapeKeydownHandler); }); }); From cabdea4e1e19255332106e885884050076086851 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 22:06:40 +0200 Subject: [PATCH 148/154] refactor(modal): destrucure ...props first on --- packages/kit-headless/src/components/modal/modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 32b1fd1c5..76248f72c 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -86,8 +86,8 @@ export const Modal = component$((props: ModalProps) => { return ( closeOnBackdropClick$(event)} > From a030c17d4346475b11fd703b8cc75199a1fcbf02 Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Mon, 23 Oct 2023 00:17:30 -0500 Subject: [PATCH 149/154] docs(docs): sheet docs examp;le --- .../headless/(components)/modal/examples.tsx | 7 + .../(components)/modal/examples/sheet.tsx | 148 ++++++++++++++++++ .../headless/(components)/modal/index.mdx | 6 + .../src/components/modal/modal-behavior.ts | 17 +- .../src/components/modal/modal.tsx | 4 +- 5 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx index b04f1aa13..d40cd18e2 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx @@ -26,6 +26,9 @@ import animationExampleCode from './examples/animation?raw'; import Transition from './examples/transition'; import transitionExampleCode from './examples/transition?raw'; +import Sheet from './examples/sheet'; +import sheetExampleCode from './examples/sheet?raw'; + import buildingBlocksSnip from './examples/building-blocks-snip?raw'; import pageLoadSnip from './examples/page-load-snip?raw'; import animationSnip from './examples/animation-snip.css?raw'; @@ -72,6 +75,10 @@ export const examples: Record = { component: , code: transitionExampleCode, }, + sheet: { + component: , + code: sheetExampleCode, + }, }; export type ShowExampleProps = { diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx new file mode 100644 index 000000000..653e8a2bb --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx @@ -0,0 +1,148 @@ +import { + component$, + useSignal, + type QwikIntrinsicElements, + useStyles$, +} from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; + +export default component$(() => { + const showSig = useSignal(false); + useStyles$(` + .sheet::backdrop { + background: hsla(0, 0%, 0%, 0.5); + } + + .sheet.modal-showing { + animation: sheetOpen 0.75s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + .sheet.modal-showing::backdrop { + animation: sheetFadeIn 0.75s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + .sheet.modal-closing { + animation: sheetClose 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + .sheet.modal-closing::backdrop { + animation: sheetFadeOut 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + @keyframes sheetOpen { + from { + opacity: 0; + transform: translateX(calc(100%)); + } + to { + opacity: 1; + transform: translateX(0%); + } + } + + @keyframes sheetClose { + from { + opacity: 1; + transform: translateX(0%); + } + to { + opacity: 0; + transform: translateX(calc(100%)); + } + } + + @keyframes sheetFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes sheetFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + `); + + return ( + <> + + + +

Edit Profile

+
+ +

+ You can update your profile here. Hit the save button when finished. +

+
+ + +
+
+ + +
+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index 2752ea0d5..a0d8f24cd 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -191,6 +191,12 @@ To add a backdrop animation, make sure to use the `::backdrop` pseudo selector t > Firefox currently does not support backdrop animations. The fallback for browsers that do not support animated backdrops is the same as a non-animated backdrop. +## Custom Modal Placement + +### Sheet + + + ## Open on page load There might come a time where a modal is needed open as soon as the page loads. Unfortunately, to create modals a client-side API is currently needed regardless of implementation. diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 22e398a40..24d3c734c 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -75,21 +75,22 @@ export function wasModalBackdropClicked( /** * Locks scrolling of the document. */ -export function lockScroll() { +export function lockScroll(scrollbar: WidthState) { + if (scrollbar.width === null) { + scrollbar.width = window.innerWidth - document.documentElement.clientWidth; + } + window.document.body.style.overflow = 'hidden'; + document.body.style.paddingRight = `${scrollbar.width}px`; } /** * Unlocks scrolling of the document. * Adjusts padding of the given scrollbar. */ -export function unlockScroll(scrollbar: WidthState) { +export function unlockScroll() { window.document.body.style.overflow = ''; - - const currentPadding = parseInt(document.body.style.paddingRight); - if (scrollbar.width) { - document.body.style.paddingRight = `${currentPadding - scrollbar.width}px`; - } + document.body.style.paddingRight = ''; } /** @@ -105,7 +106,7 @@ export function adjustScrollbar(scrollbar: WidthState, modal: HTMLDialogElement) scrollbar.width = window.innerWidth - document.documentElement.clientWidth; } - modal.style.left = `0px`; + modal.style.left = 0 + 'px'; document.body.style.paddingRight = `${scrollbar.width}px`; } diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 76248f72c..83c5734e1 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -61,9 +61,9 @@ export const Modal = component$((props: ModalProps) => { props.onShow$?.(); adjustScrollbar(scrollbar, modal); activateFocusTrap(focusTrap); - lockScroll(); + lockScroll(scrollbar); } else { - unlockScroll(scrollbar); + unlockScroll(); closeModal(modal); props.onClose$?.(); } From 426bf48f356597f9eda60c48f80c5caa94de9807 Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Mon, 23 Oct 2023 15:47:50 -0500 Subject: [PATCH 150/154] fix(modal): fixing scrollbar flickers --- .../(components)/modal/examples/animation.css | 4 +- .../(components)/modal/examples/sheet.tsx | 4 +- .../modal/examples/transition.tsx | 2 +- package.json | 2 + .../src/components/modal/modal-behavior.ts | 64 ++----------------- .../src/components/modal/modal.tsx | 17 ++--- pnpm-lock.yaml | 14 ++++ 7 files changed, 33 insertions(+), 74 deletions(-) diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css index c0b34c92f..dae0d0d38 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/animation.css @@ -11,11 +11,11 @@ } .my-animation.modal-closing { - animation: modalClose 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1); + animation: modalClose 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1); } .my-animation.modal-closing::backdrop { - animation: modalFadeOut 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1); + animation: modalFadeOut 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1); } @keyframes modalOpen { diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx index 653e8a2bb..e19c2957e 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx @@ -22,11 +22,11 @@ export default component$(() => { } .sheet.modal-closing { - animation: sheetClose 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1); + animation: sheetClose 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1); } .sheet.modal-closing::backdrop { - animation: sheetFadeOut 0.45s forwards cubic-bezier(0.6, 0.6, 0, 1); + animation: sheetFadeOut 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1); } @keyframes sheetOpen { diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx index 4608bf2e0..c6ab7883a 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/transition.tsx @@ -16,7 +16,7 @@ export default component$(() => { .my-transition, .my-transition::backdrop { opacity: 0; - transition: opacity 500ms ease; + transition: opacity 300ms ease; } .my-transition.modal-showing, .my-transition.modal-showing::backdrop { diff --git a/package.json b/package.json index 7fb1e8b39..bda8ce4e6 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@storybook/test-runner": "^0.13.0", "@storybook/testing-library": "^0.2.0", "@testing-library/cypress": "9.0.0", + "@types/body-scroll-lock": "3.1.1", "@types/eslint": "^8.44.2", "@types/node": "^20.5.7", "@typescript-eslint/eslint-plugin": "^5", @@ -64,6 +65,7 @@ "all-contributors-cli": "^6.26.1", "autoprefixer": "^10.4.15", "axe-core": "^4.7.2", + "body-scroll-lock": "4.0.0-beta.0", "chromatic": "^6.24.1", "clipboard-copy": "4.0.1", "commitizen": "^4.3.0", diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index 24d3c734c..cfe31d0cd 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -5,6 +5,8 @@ export type WidthState = { width: number | null; }; +import { clearAllBodyScrollLocks } from 'body-scroll-lock'; + /** * Traps the focus of the given Modal * @returns FocusTrap @@ -72,44 +74,6 @@ export function wasModalBackdropClicked( return wasBackdropClicked; } -/** - * Locks scrolling of the document. - */ -export function lockScroll(scrollbar: WidthState) { - if (scrollbar.width === null) { - scrollbar.width = window.innerWidth - document.documentElement.clientWidth; - } - - window.document.body.style.overflow = 'hidden'; - document.body.style.paddingRight = `${scrollbar.width}px`; -} - -/** - * Unlocks scrolling of the document. - * Adjusts padding of the given scrollbar. - */ -export function unlockScroll() { - window.document.body.style.overflow = ''; - document.body.style.paddingRight = ''; -} - -/** - * When the Modal is opened we are disabling scrolling. - * This means the scrollbar will vanish. - * The scrollbar has a width and causes the Modal to reposition. - * - * That's why we take the scrollbar-width into account so that the - * Modal does not jump to the right. - */ -export function adjustScrollbar(scrollbar: WidthState, modal: HTMLDialogElement) { - if (scrollbar.width === null) { - scrollbar.width = window.innerWidth - document.documentElement.clientWidth; - } - - modal.style.left = 0 + 'px'; - document.body.style.paddingRight = `${scrollbar.width}px`; -} - export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) { return function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') { @@ -120,27 +84,6 @@ export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) }; } -/** - * When the Modal is closed we are enabling scrolling. - * This means the scrollbar will reappear in the browser. - * The scrollbar has a width and causes the Modal to reposition. - * - * That's why we take the scrollbar-width into account so that the - * Modal remains in the same position as before. - */ -export function keepModalInPlaceWhileScrollbarReappears( - scrollbar: WidthState, - modal?: HTMLDialogElement, -) { - if (!modal) return; - - if (scrollbar.width) { - const modalLeft = parseInt(modal.style.left); - - modal.style.left = `${scrollbar.width - modalLeft}px`; - } -} - /* * Adds CSS-Class to support modal-opening-animation */ @@ -162,11 +105,13 @@ export function supportClosingAnimation( const { animationDuration, transitionDuration } = getComputedStyle(modal); const runAnimationEnd = () => { + clearAllBodyScrollLocks(); modal.classList.remove('modal-closing'); afterAnimate(); }; const runTransitionEnd = () => { + clearAllBodyScrollLocks(); modal.classList.remove('modal-closing'); afterAnimate(); }; @@ -177,6 +122,7 @@ export function supportClosingAnimation( modal.addEventListener('transitionend', runTransitionEnd, { once: true }); } else { modal.classList.remove('modal-closing'); + clearAllBodyScrollLocks(); afterAnimate(); } } diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 83c5734e1..89a7eb853 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -11,20 +11,17 @@ import { useTask$, } from '@builder.io/qwik'; import { - WidthState, activateFocusTrap, - adjustScrollbar, closeModal, deactivateFocusTrap, - keepModalInPlaceWhileScrollbarReappears, - lockScroll, overrideNativeDialogEscapeBehaviorWith, showModal, trapFocus, - unlockScroll, wasModalBackdropClicked, } from './modal-behavior'; +import { disableBodyScroll, type BodyScrollOptions } from 'body-scroll-lock'; + import styles from './modal.css?inline'; export type ModalProps = Omit & { @@ -38,7 +35,10 @@ export type ModalProps = Omit & { export const Modal = component$((props: ModalProps) => { useStyles$(styles); const modalRefSig = useSignal(); - const scrollbar: WidthState = { width: null }; + + const scrollOptions: BodyScrollOptions = { + reserveScrollBarGap: true, + }; const { 'bind:show': showSig } = props; @@ -58,19 +58,16 @@ export const Modal = component$((props: ModalProps) => { if (isOpen) { showModal(modal); + disableBodyScroll(modal, scrollOptions); props.onShow$?.(); - adjustScrollbar(scrollbar, modal); activateFocusTrap(focusTrap); - lockScroll(scrollbar); } else { - unlockScroll(); closeModal(modal); props.onClose$?.(); } cleanup(() => { deactivateFocusTrap(focusTrap); - keepModalInPlaceWhileScrollbarReappears(scrollbar, modalRefSig.value); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 347d454b9..3b29ea8e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: '@testing-library/cypress': specifier: 9.0.0 version: 9.0.0(cypress@13.0.0) + '@types/body-scroll-lock': + specifier: 3.1.1 + version: 3.1.1 '@types/eslint': specifier: ^8.44.2 version: 8.44.2 @@ -141,6 +144,9 @@ importers: axe-core: specifier: ^4.7.2 version: 4.7.2 + body-scroll-lock: + specifier: 4.0.0-beta.0 + version: 4.0.0-beta.0 chromatic: specifier: ^6.24.1 version: 6.24.1 @@ -8587,6 +8593,10 @@ packages: '@types/node': 20.5.9 dev: true + /@types/body-scroll-lock@3.1.1: + resolution: {integrity: sha512-W5tM34rxQzEGXomqg5A2V8pHl/NQDCCuf7pygI2+2SZEbFB6zPDuX3uwkUxlVLIF31fob4elEjiMLliq3szk0g==} + dev: true + /@types/cacheable-request@6.0.3: resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} dependencies: @@ -10415,6 +10425,10 @@ packages: - supports-color dev: true + /body-scroll-lock@4.0.0-beta.0: + resolution: {integrity: sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==} + dev: true + /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: true From a83d3a5a251c52ffdafe56e70d7e7a42c07464da Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Mon, 23 Oct 2023 17:19:56 -0500 Subject: [PATCH 151/154] docs(docs): fixing the header, making it sticky so there is no layout shift --- apps/website/src/routes/_components/header/header.tsx | 2 +- apps/website/src/routes/layout.tsx | 2 +- .../src/components/modal/modal-behavior.ts | 11 ++++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/website/src/routes/_components/header/header.tsx b/apps/website/src/routes/_components/header/header.tsx index 6cd7cd778..096c06719 100644 --- a/apps/website/src/routes/_components/header/header.tsx +++ b/apps/website/src/routes/_components/header/header.tsx @@ -64,7 +64,7 @@ export default component$( return (
{ <>
-
+
diff --git a/packages/kit-headless/src/components/modal/modal-behavior.ts b/packages/kit-headless/src/components/modal/modal-behavior.ts index cfe31d0cd..72fd019d8 100644 --- a/packages/kit-headless/src/components/modal/modal-behavior.ts +++ b/packages/kit-headless/src/components/modal/modal-behavior.ts @@ -25,9 +25,6 @@ export function activateFocusTrap(focusTrap: FocusTrap | null) { } } -/** - * Deactivates the given FocusTrap - */ export function deactivateFocusTrap(focusTrap: FocusTrap | null) { focusTrap?.deactivate(); focusTrap = null; @@ -84,14 +81,14 @@ export function overrideNativeDialogEscapeBehaviorWith(continuation: () => void) }; } -/* +/** * Adds CSS-Class to support modal-opening-animation */ export function supportShowAnimation(modal: HTMLDialogElement) { modal.classList.add('modal-showing'); } -/* +/** * Listens for animation/transition events in order to * remove Animation-CSS-Classes after animation/transition ended. */ @@ -105,14 +102,14 @@ export function supportClosingAnimation( const { animationDuration, transitionDuration } = getComputedStyle(modal); const runAnimationEnd = () => { - clearAllBodyScrollLocks(); modal.classList.remove('modal-closing'); + clearAllBodyScrollLocks(); afterAnimate(); }; const runTransitionEnd = () => { - clearAllBodyScrollLocks(); modal.classList.remove('modal-closing'); + clearAllBodyScrollLocks(); afterAnimate(); }; From fafbb312830ccc5b19e5823327933cb4b2b80865 Mon Sep 17 00:00:00 2001 From: thejackshelton Date: Mon, 23 Oct 2023 20:29:10 -0500 Subject: [PATCH 152/154] docs(docs): sheet examples --- .../headless/(components)/modal/examples.tsx | 7 + .../modal/examples/bottom-sheet.tsx | 148 ++++++++++++++++++ .../(components)/modal/examples/sheet.tsx | 6 +- .../headless/(components)/modal/index.mdx | 12 +- 4 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/(components)/modal/examples/bottom-sheet.tsx diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx index d40cd18e2..73476d07a 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples.tsx @@ -29,6 +29,9 @@ import transitionExampleCode from './examples/transition?raw'; import Sheet from './examples/sheet'; import sheetExampleCode from './examples/sheet?raw'; +import BottomSheet from './examples/bottom-sheet'; +import bottomSheetExampleCode from './examples/bottom-sheet?raw'; + import buildingBlocksSnip from './examples/building-blocks-snip?raw'; import pageLoadSnip from './examples/page-load-snip?raw'; import animationSnip from './examples/animation-snip.css?raw'; @@ -79,6 +82,10 @@ export const examples: Record = { component: , code: sheetExampleCode, }, + bottomSheet: { + component: , + code: bottomSheetExampleCode, + }, }; export type ShowExampleProps = { diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/bottom-sheet.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/bottom-sheet.tsx new file mode 100644 index 000000000..48ecb961f --- /dev/null +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/bottom-sheet.tsx @@ -0,0 +1,148 @@ +import { + component$, + useSignal, + type QwikIntrinsicElements, + useStyles$, +} from '@builder.io/qwik'; +import { Modal, ModalContent, ModalFooter, ModalHeader } from '@qwik-ui/headless'; + +export default component$(() => { + const showSig = useSignal(false); + useStyles$(` + .bottom-sheet::backdrop { + background: hsla(0, 0%, 0%, 0.5); + } + + .bottom-sheet.modal-showing { + animation: bottomSheetOpen 0.75s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + .bottom-sheet.modal-showing::backdrop { + animation: sheetFadeIn 0.75s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + .bottom-sheet.modal-closing { + animation: bottomSheetClose 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + .bottom-sheet.modal-closing::backdrop { + animation: sheetFadeOut 0.35s forwards cubic-bezier(0.6, 0.6, 0, 1); + } + + @keyframes bottomSheetOpen { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0%); + } + } + + @keyframes bottomSheetClose { + from { + opacity: 1; + transform: translateY(0%); + } + to { + opacity: 0; + transform: translateY(100%); + } + } + + @keyframes sheetFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes sheetFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } + } + + `); + + return ( + <> + + + +

Edit Profile

+
+ +

+ You can update your profile here. Hit the save button when finished. +

+
+ + +
+
+ + +
+
+ + + + + +
+ + ); +}); + +export function CloseIcon(props: QwikIntrinsicElements['svg'], key: string) { + return ( + + + + ); +} diff --git a/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx b/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx index e19c2957e..0e626b4ab 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx +++ b/apps/website/src/routes/docs/headless/(components)/modal/examples/sheet.tsx @@ -32,7 +32,7 @@ export default component$(() => { @keyframes sheetOpen { from { opacity: 0; - transform: translateX(calc(100%)); + transform: translateX(100%); } to { opacity: 1; @@ -47,7 +47,7 @@ export default component$(() => { } to { opacity: 0; - transform: translateX(calc(100%)); + transform: translateX(100%); } } @@ -83,7 +83,7 @@ export default component$(() => {

Edit Profile

diff --git a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx index a0d8f24cd..6f8c40359 100644 --- a/apps/website/src/routes/docs/headless/(components)/modal/index.mdx +++ b/apps/website/src/routes/docs/headless/(components)/modal/index.mdx @@ -191,12 +191,20 @@ To add a backdrop animation, make sure to use the `::backdrop` pseudo selector t > Firefox currently does not support backdrop animations. The fallback for browsers that do not support animated backdrops is the same as a non-animated backdrop. -## Custom Modal Placement +## Sheets -### Sheet +Sheets are a type of modal/overlay used to provide temporary access to important information, while also being easily dismissible. + +### Side Sheet +### Bottom Sheet + + + +Bottom sheets are more prevalent in mobile applications, usually to simplify UI. That said, feel free to use them wherever fits best! + ## Open on page load There might come a time where a modal is needed open as soon as the page loads. Unfortunately, to create modals a client-side API is currently needed regardless of implementation. From e9c57a4674229d09a2a139621f1470fc47cd295c Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Fri, 20 Oct 2023 22:08:28 +0200 Subject: [PATCH 153/154] refactor(modal): improve naming of helper --- packages/kit-headless/src/components/modal/modal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 89a7eb853..6078b29ad 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -14,6 +14,8 @@ import { activateFocusTrap, closeModal, deactivateFocusTrap, + keepModalInPlaceWhileScrollbarReappears as keepModalInPlaceWhenScrollbarReappears, + lockScroll, overrideNativeDialogEscapeBehaviorWith, showModal, trapFocus, @@ -68,6 +70,7 @@ export const Modal = component$((props: ModalProps) => { cleanup(() => { deactivateFocusTrap(focusTrap); + keepModalInPlaceWhenScrollbarReappears(scrollbar, modalRefSig.value); }); }); From f50b16ff82fa647a28693a404806288394df0861 Mon Sep 17 00:00:00 2001 From: Gregor Woiwode Date: Tue, 24 Oct 2023 09:34:25 +0200 Subject: [PATCH 154/154] refactor(modal): inline BodyScrollOptions --- packages/kit-headless/src/components/modal/modal.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/kit-headless/src/components/modal/modal.tsx b/packages/kit-headless/src/components/modal/modal.tsx index 6078b29ad..db7312806 100644 --- a/packages/kit-headless/src/components/modal/modal.tsx +++ b/packages/kit-headless/src/components/modal/modal.tsx @@ -10,19 +10,18 @@ import { useStyles$, useTask$, } from '@builder.io/qwik'; + import { activateFocusTrap, closeModal, deactivateFocusTrap, - keepModalInPlaceWhileScrollbarReappears as keepModalInPlaceWhenScrollbarReappears, - lockScroll, overrideNativeDialogEscapeBehaviorWith, showModal, trapFocus, wasModalBackdropClicked, } from './modal-behavior'; -import { disableBodyScroll, type BodyScrollOptions } from 'body-scroll-lock'; +import { disableBodyScroll } from 'body-scroll-lock'; import styles from './modal.css?inline'; @@ -38,10 +37,6 @@ export const Modal = component$((props: ModalProps) => { useStyles$(styles); const modalRefSig = useSignal(); - const scrollOptions: BodyScrollOptions = { - reserveScrollBarGap: true, - }; - const { 'bind:show': showSig } = props; useTask$(async function toggleModal({ track, cleanup }) { @@ -60,7 +55,7 @@ export const Modal = component$((props: ModalProps) => { if (isOpen) { showModal(modal); - disableBodyScroll(modal, scrollOptions); + disableBodyScroll(modal, { reserveScrollBarGap: true }); props.onShow$?.(); activateFocusTrap(focusTrap); } else { @@ -70,7 +65,6 @@ export const Modal = component$((props: ModalProps) => { cleanup(() => { deactivateFocusTrap(focusTrap); - keepModalInPlaceWhenScrollbarReappears(scrollbar, modalRefSig.value); }); });