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