ui/modal/select: fix z-index stacking
This commit is contained in:
@@ -37,7 +37,8 @@
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
@apply absolute left-0 top-0 z-50 size-full bg-black opacity-40;
|
||||
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TagProps } from "@/src/components/Tag/Tag";
|
||||
import { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { fn } from "storybook/test";
|
||||
import { Modal, ModalContext, ModalProps } from "@/src/components/Modal/Modal";
|
||||
import { Modal, ModalProps } from "@/src/components/Modal/Modal";
|
||||
import { Fieldset, FieldsetFieldProps } from "@/src/components/Form/Fieldset";
|
||||
import { TextInput } from "@/src/components/Form/TextInput";
|
||||
import { TextArea } from "@/src/components/Form/TextArea";
|
||||
@@ -21,7 +21,7 @@ export const Default: Story = {
|
||||
args: {
|
||||
title: "Example Modal",
|
||||
onClose: fn(),
|
||||
children: ({ close }: ModalContext) => (
|
||||
children: (
|
||||
<form class="flex flex-col gap-5">
|
||||
<Fieldset legend="General">
|
||||
{(props: FieldsetFieldProps) => (
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Component, JSX, Show } from "solid-js";
|
||||
import {
|
||||
Component,
|
||||
JSX,
|
||||
Show,
|
||||
createContext,
|
||||
createSignal,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { Dialog as KDialog } from "@kobalte/core/dialog";
|
||||
import styles from "./Modal.module.css";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
@@ -6,15 +13,25 @@ import Icon from "../Icon/Icon";
|
||||
import cx from "classnames";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
export interface ModalContext {
|
||||
close(): void;
|
||||
}
|
||||
export type ModalContextType = {
|
||||
portalRef: HTMLDivElement;
|
||||
};
|
||||
|
||||
const ModalContext = createContext<unknown>();
|
||||
|
||||
export const useModalContext = () => {
|
||||
const context = useContext(ModalContext);
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
return context as ModalContextType;
|
||||
};
|
||||
|
||||
export interface ModalProps {
|
||||
id?: string;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
children: (ctx: ModalContext) => JSX.Element;
|
||||
children: JSX.Element;
|
||||
mount?: Node;
|
||||
class?: string;
|
||||
metaHeader?: Component;
|
||||
@@ -23,6 +40,7 @@ export interface ModalProps {
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const [portalRef, setPortalRef] = createSignal<HTMLDivElement>();
|
||||
return (
|
||||
<Show when={props.open}>
|
||||
<KDialog id={props.id} open={props.open} modal={true}>
|
||||
@@ -60,12 +78,11 @@ export const Modal = (props: ModalProps) => {
|
||||
<div
|
||||
class={styles.modal_body}
|
||||
data-no-padding={props.disablePadding}
|
||||
ref={setPortalRef}
|
||||
>
|
||||
{props.children({
|
||||
close: () => {
|
||||
props.onClose();
|
||||
},
|
||||
})}
|
||||
<ModalContext.Provider value={{ portalRef: portalRef()! }}>
|
||||
{props.children}
|
||||
</ModalContext.Provider>
|
||||
</div>
|
||||
</KDialog.Content>
|
||||
</div>
|
||||
|
||||
@@ -60,11 +60,11 @@
|
||||
|
||||
/* Option elements (typically <li>) */
|
||||
& [role="option"] {
|
||||
@apply p-2 rounded-sm flex items-center gap-1 flex-shrink-0;
|
||||
@apply w-full p-2 rounded-sm flex items-center gap-1 flex-shrink-0;
|
||||
|
||||
&[data-highlighted],
|
||||
&:focus-visible {
|
||||
@apply outline outline-1 outline-inv-2;
|
||||
@apply outline outline-1 outline-inv-2 outline-offset-[-1px];
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -77,6 +77,10 @@
|
||||
}
|
||||
|
||||
& [role="listbox"] {
|
||||
width: var(--kb-popper-anchor-width);
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
&:focus-visible {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,10 @@ export const Default: Story = {
|
||||
description: "Choose your favorite pet from the list",
|
||||
},
|
||||
options: [
|
||||
{ value: "dog", label: "Doggy" },
|
||||
{
|
||||
value: "dog",
|
||||
label: "DoggyDoggyDoggyDoggyDoggyDoggy DoggyDoggyDoggyDoggyDoggy",
|
||||
},
|
||||
{ value: "cat", label: "Catty" },
|
||||
{ value: "fish", label: "Fishy" },
|
||||
{ value: "bird", label: "Birdy" },
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createEffect, createSignal, JSX, Show, splitProps } from "solid-js";
|
||||
import styles from "./Select.module.css";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import cx from "classnames";
|
||||
import { useModalContext } from "../Modal/Modal";
|
||||
|
||||
export interface Option {
|
||||
value: string;
|
||||
@@ -79,6 +80,13 @@ export const Select = (props: SelectProps) => {
|
||||
setValue(options().find((option) => props.value === option.value));
|
||||
});
|
||||
|
||||
const modalContext = useModalContext();
|
||||
const defaultMount =
|
||||
props.portalProps?.mount || modalContext?.portalRef || document.body;
|
||||
|
||||
createEffect(() => {
|
||||
console.debug("Select component mounted at:", defaultMount);
|
||||
});
|
||||
return (
|
||||
<KSelect
|
||||
{...root}
|
||||
@@ -174,12 +182,40 @@ export const Select = (props: SelectProps) => {
|
||||
</KSelect.Icon>
|
||||
</KSelect.Trigger>
|
||||
</Orienter>
|
||||
<KSelect.Portal {...props.portalProps}>
|
||||
<KSelect.Portal mount={defaultMount} {...props.portalProps}>
|
||||
<KSelect.Content
|
||||
class={styles.options_content}
|
||||
style={{ "--z-index": zIndex() }}
|
||||
>
|
||||
<KSelect.Listbox />
|
||||
<KSelect.Listbox>
|
||||
{() => (
|
||||
<KSelect.Trigger
|
||||
class={cx(styles.trigger)}
|
||||
style={{ "--z-index": zIndex() }}
|
||||
data-loading={loading() || undefined}
|
||||
>
|
||||
<KSelect.Value<Option>>
|
||||
{(state) => (
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="bold"
|
||||
class="flex w-full items-center"
|
||||
>
|
||||
{state.selectedOption().label}
|
||||
</Typography>
|
||||
)}
|
||||
</KSelect.Value>
|
||||
<KSelect.Icon
|
||||
as="button"
|
||||
class={styles.icon}
|
||||
data-loading={loading() || undefined}
|
||||
>
|
||||
<Icon icon="Expand" color="inherit" />
|
||||
</KSelect.Icon>
|
||||
</KSelect.Trigger>
|
||||
)}
|
||||
</KSelect.Listbox>
|
||||
</KSelect.Content>
|
||||
</KSelect.Portal>
|
||||
{/* TODO: Display error next to the problem */}
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.createBackdrop {
|
||||
@apply absolute top-0 left-0 w-full h-svh flex justify-center items-center backdrop-blur-0 z-50;
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.createModal {
|
||||
@apply min-w-96;
|
||||
}
|
||||
|
||||
@@ -54,55 +54,43 @@ interface MockProps {
|
||||
}
|
||||
|
||||
const MockCreateMachine = (props: MockProps) => {
|
||||
let container: Node;
|
||||
|
||||
const [form, { Form, Field, FieldArray }] = createForm<CreateFormValues>();
|
||||
|
||||
return (
|
||||
<div ref={(el) => (container = el)} class={cx(styles.createBackdrop)}>
|
||||
<Modal
|
||||
open={true}
|
||||
mount={container!}
|
||||
onClose={() => {
|
||||
reset(form);
|
||||
props.onClose();
|
||||
}}
|
||||
class={cx(styles.createModal)}
|
||||
title="Create Machine"
|
||||
>
|
||||
{() => (
|
||||
<Form class="flex flex-col" onSubmit={props.onSubmit}>
|
||||
<Field name="name">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...field}
|
||||
label="Name"
|
||||
size="s"
|
||||
required={true}
|
||||
input={{ ...props, placeholder: "name", autofocus: true }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div class="mt-4 flex w-full items-center justify-end gap-4">
|
||||
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Modal
|
||||
open={true}
|
||||
onClose={() => {
|
||||
reset(form);
|
||||
props.onClose();
|
||||
}}
|
||||
class={cx(styles.createModal)}
|
||||
title="Create Machine"
|
||||
>
|
||||
<Form class="flex flex-col" onSubmit={props.onSubmit}>
|
||||
<Field name="name">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<TextInput
|
||||
{...field}
|
||||
label="Name"
|
||||
size="s"
|
||||
type="submit"
|
||||
hierarchy="primary"
|
||||
onClick={close}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
required={true}
|
||||
input={{ ...props, placeholder: "name", autofocus: true }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div class="mt-4 flex w-full items-center justify-end gap-4">
|
||||
<Button size="s" hierarchy="secondary" onClick={props.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="s" type="submit" hierarchy="primary" onClick={close}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ type Story = StoryObj<typeof InstallModal>;
|
||||
export const Init: Story = {
|
||||
description: "Welcome step for the installation workflow",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "init",
|
||||
},
|
||||
@@ -208,6 +209,7 @@ export const Init: Story = {
|
||||
export const CreateInstallerProse: Story = {
|
||||
description: "Prose step for creating an installer",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "create:prose",
|
||||
},
|
||||
@@ -215,6 +217,7 @@ export const CreateInstallerProse: Story = {
|
||||
export const CreateInstallerImage: Story = {
|
||||
description: "Configure the image to install",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "create:image",
|
||||
},
|
||||
@@ -222,6 +225,7 @@ export const CreateInstallerImage: Story = {
|
||||
export const CreateInstallerDisk: Story = {
|
||||
description: "Select a disk to install the image on",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "create:disk",
|
||||
},
|
||||
@@ -229,6 +233,7 @@ export const CreateInstallerDisk: Story = {
|
||||
export const CreateInstallerProgress: Story = {
|
||||
description: "Showed while the USB stick is being flashed",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "create:progress",
|
||||
},
|
||||
@@ -236,6 +241,7 @@ export const CreateInstallerProgress: Story = {
|
||||
export const CreateInstallerDone: Story = {
|
||||
description: "Installation done step",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "create:done",
|
||||
},
|
||||
@@ -243,6 +249,7 @@ export const CreateInstallerDone: Story = {
|
||||
export const InstallConfigureAddress: Story = {
|
||||
description: "Installation configure address step",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "install:address",
|
||||
},
|
||||
@@ -250,6 +257,7 @@ export const InstallConfigureAddress: Story = {
|
||||
export const InstallCheckHardware: Story = {
|
||||
description: "Installation check hardware step",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "install:check-hardware",
|
||||
},
|
||||
@@ -257,6 +265,7 @@ export const InstallCheckHardware: Story = {
|
||||
export const InstallSelectDisk: Story = {
|
||||
description: "Select disk to install the system on",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "install:disk",
|
||||
},
|
||||
@@ -264,6 +273,7 @@ export const InstallSelectDisk: Story = {
|
||||
export const InstallVars: Story = {
|
||||
description: "Fill required credentials and data for the installation",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "install:data",
|
||||
},
|
||||
@@ -271,6 +281,7 @@ export const InstallVars: Story = {
|
||||
export const InstallSummary: Story = {
|
||||
description: "Summary of the installation steps",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "install:summary",
|
||||
},
|
||||
@@ -278,6 +289,7 @@ export const InstallSummary: Story = {
|
||||
export const InstallProgress: Story = {
|
||||
description: "Shown while the installation is in progress",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "install:progress",
|
||||
},
|
||||
@@ -285,6 +297,7 @@ export const InstallProgress: Story = {
|
||||
export const InstallDone: Story = {
|
||||
description: "Shown after the installation is done",
|
||||
args: {
|
||||
open: true,
|
||||
machineName: "Test Machine",
|
||||
initialStep: "install:done",
|
||||
},
|
||||
|
||||
@@ -97,7 +97,7 @@ export const InstallModal = (props: InstallModalProps) => {
|
||||
// @ts-expect-error some steps might not have
|
||||
disablePadding={stepper.currentStep()?.isSplash}
|
||||
>
|
||||
{(ctx) => <InstallStepper onDone={ctx.close} />}
|
||||
<InstallStepper onDone={() => props.onClose} />
|
||||
</Modal>
|
||||
</StepperProvider>
|
||||
);
|
||||
|
||||
@@ -142,18 +142,11 @@ const ConfigureImage = () => {
|
||||
throw new Error("No data returned from api call");
|
||||
};
|
||||
|
||||
let content: Node;
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} class="h-full">
|
||||
<StepLayout
|
||||
body={
|
||||
<div
|
||||
class="flex flex-col gap-2"
|
||||
ref={(el) => {
|
||||
content = el;
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Fieldset>
|
||||
<Field name="ssh_key">
|
||||
{(field, input) => (
|
||||
|
||||
Reference in New Issue
Block a user