feat(components): add InlineWrapper and related styles for improved form handling
This commit is contained in:
38
src/FormerControllers/Inputs/InlineWapper.module.css
Normal file
38
src/FormerControllers/Inputs/InlineWapper.module.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.prompt {
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-right: 0px;
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
border: 1px solid #373a40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:not([data-promptArea]) {
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-promptArea] {
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-disabled] {
|
||||||
|
color: black;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
border: 1px solid #373a40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
176
src/FormerControllers/Inputs/InlineWrapper.tsx
Normal file
176
src/FormerControllers/Inputs/InlineWrapper.tsx
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
type FlexProps,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Title,
|
||||||
|
type TitleProps,
|
||||||
|
Tooltip,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import classes from './InlineWapper.module.css'
|
||||||
|
|
||||||
|
interface InlineWrapperCallbackProps extends Partial<InlineWrapperPropsOnly> {
|
||||||
|
classNames: React.CSSProperties
|
||||||
|
dataCssProps?: Record<string, any>
|
||||||
|
size: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InlineWrapperProps extends InlineWrapperPropsOnly{
|
||||||
|
children?: ((props: InlineWrapperCallbackProps) => ReactNode) | ReactNode
|
||||||
|
}
|
||||||
|
interface InlineWrapperPropsOnly {
|
||||||
|
error?: ReactNode | string
|
||||||
|
flexProps?: FlexProps
|
||||||
|
label: ReactNode | string
|
||||||
|
labelProps?: TitleProps
|
||||||
|
promptArea?: ((props: InlineWrapperCallbackProps) => ReactNode) | ReactNode
|
||||||
|
promptWidth?: FlexProps['w']
|
||||||
|
required?: boolean
|
||||||
|
rightSection?: ((props: InlineWrapperCallbackProps) => ReactNode) | ReactNode
|
||||||
|
styles?: React.CSSProperties
|
||||||
|
tooltip?: string
|
||||||
|
value?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function InlineWrapper(props: InlineWrapperProps) {
|
||||||
|
return (
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Flex
|
||||||
|
gap={0}
|
||||||
|
h={undefined}
|
||||||
|
m={0}
|
||||||
|
mb={0}
|
||||||
|
p={0}
|
||||||
|
w={undefined}
|
||||||
|
wrap='nowrap'
|
||||||
|
{...props.flexProps}
|
||||||
|
bg={'var(--input-background)'}
|
||||||
|
>
|
||||||
|
{props.promptWidth && props.promptWidth !== 0 ? <Prompt {...props} /> : null}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: 0,
|
||||||
|
flex: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof props.children === 'function' ? (
|
||||||
|
props.children({ ...props, classNames: classes, size: 'xs' })
|
||||||
|
) : typeof props.children === 'object' && React.isValidElement(props.children) ? (
|
||||||
|
<props.children.type classNames={classes} size='xs' {...(typeof props.children.props === "object" ? props.children.props : {})} />
|
||||||
|
) : (
|
||||||
|
props.children
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!props.rightSection ? undefined : typeof props.rightSection === 'function' ? (
|
||||||
|
props.rightSection({
|
||||||
|
...props,
|
||||||
|
classNames: classes,
|
||||||
|
size: 'xs',
|
||||||
|
})
|
||||||
|
) : typeof props.rightSection === 'object' && React.isValidElement(props.rightSection) ? (
|
||||||
|
<props.rightSection.type classNames={classes} size='xs' {...(typeof props.rightSection.props === "object" ? props.rightSection.props : {})} />
|
||||||
|
) : (
|
||||||
|
props.rightSection
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{/* <ErrorComponent {...props} /> */}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValueEmpty(inputValue: any) {
|
||||||
|
if (inputValue === null || inputValue === undefined) return true
|
||||||
|
if (typeof inputValue === 'number') {
|
||||||
|
if (inputValue === 0) return false
|
||||||
|
} else if (typeof inputValue === 'string' || inputValue === '') {
|
||||||
|
return inputValue.trim() === ''
|
||||||
|
} else if (inputValue instanceof File) {
|
||||||
|
return inputValue.size === 0
|
||||||
|
} else if (inputValue.target) {
|
||||||
|
return isValueEmpty(inputValue.target?.value)
|
||||||
|
} else if (inputValue.constructor?.name === 'Date') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Prompt(props: Partial<InlineWrapperProps>) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.tooltip ? (
|
||||||
|
<Tooltip label={props.tooltip}>
|
||||||
|
<PromptDetail {...props} />
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<PromptDetail {...props} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PromptDetail(props: Partial<InlineWrapperProps>) {
|
||||||
|
const colors = useColors(props)
|
||||||
|
return props.promptArea ? (
|
||||||
|
<Box maw={props.promptWidth} w={'100%'}>
|
||||||
|
{!props.promptArea ? undefined : typeof props.promptArea === 'function' ? (
|
||||||
|
props.promptArea({
|
||||||
|
...props,
|
||||||
|
classNames: classes,
|
||||||
|
dataCssProps: { 'data-promptArea': true },
|
||||||
|
size: 'xs',
|
||||||
|
})
|
||||||
|
) : typeof props.rightSection === 'object' && React.isValidElement(props.promptArea) ? (
|
||||||
|
<props.promptArea.type
|
||||||
|
classNames={classes}
|
||||||
|
data-promptArea='true'
|
||||||
|
size='xs'
|
||||||
|
{...(typeof props.promptArea?.props === "object" ? props.promptArea.props : {})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
props.promptArea
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Paper
|
||||||
|
bg={colors.paperColor}
|
||||||
|
className={classes.prompt}
|
||||||
|
px='md'
|
||||||
|
w={props.promptWidth}
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
<Center h='100%' style={{ justifyContent: 'start' }} w='100%'>
|
||||||
|
<Title c={colors.titleColor} fz='xs' order={6} {...props.labelProps}>
|
||||||
|
{props.label}
|
||||||
|
{props.required && isValueEmpty(props.value) && <span style={{ color: 'red' }}>*</span>}
|
||||||
|
</Title>
|
||||||
|
</Center>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useColors(props: Partial<InlineWrapperProps>) {
|
||||||
|
const { colorScheme } = useMantineColorScheme()
|
||||||
|
|
||||||
|
let titleColor = colorScheme === 'dark' ? 'dark.0' : 'gray.8'
|
||||||
|
let paperColor = colorScheme === 'dark' ? 'dark.7' : 'gray.1'
|
||||||
|
|
||||||
|
if (props.required && isValueEmpty(props.value)) {
|
||||||
|
paperColor = colorScheme === 'dark' ? '#413012e7' : 'yellow.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.error) {
|
||||||
|
paperColor = colorScheme === 'dark' ? 'red.7' : 'red.0'
|
||||||
|
titleColor = colorScheme === 'dark' ? 'red.0' : 'red.9'
|
||||||
|
}
|
||||||
|
return { paperColor, titleColor }
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InlineWrapper }
|
||||||
|
export type { InlineWrapperProps }
|
||||||
50
src/FormerControllers/stories/Formers.goapi.stories.tsx
Normal file
50
src/FormerControllers/stories/Formers.goapi.stories.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//@ts-nocheck
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
import { Stack } from '@mantine/core';
|
||||||
|
import { fn } from 'storybook/test';
|
||||||
|
|
||||||
|
import { Former, NativeSelectCtrl, TextInputCtrl } from '../../lib';
|
||||||
|
import { InlineWrapper } from '../Inputs/InlineWrapper';
|
||||||
|
import NumberInputCtrl from '../Inputs/NumberInputCtrl';
|
||||||
|
|
||||||
|
const Renderable = () => {
|
||||||
|
return (
|
||||||
|
<Former>
|
||||||
|
<Stack h="100%" mih="400px" miw="400px" w="100%">
|
||||||
|
<TextInputCtrl label="Test" name="test" />
|
||||||
|
<NumberInputCtrl label="AgeTest" name="age" />
|
||||||
|
<InlineWrapper label="Select One" promptWidth={200}>
|
||||||
|
<NativeSelectCtrl data={["One","Two","Three"]} name="option1"/>
|
||||||
|
</InlineWrapper>
|
||||||
|
</Stack>
|
||||||
|
</Former>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
|
||||||
|
args: { onClick: fn() },
|
||||||
|
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
||||||
|
argTypes: {
|
||||||
|
backgroundColor: { control: 'color' },
|
||||||
|
},
|
||||||
|
component: Renderable,
|
||||||
|
parameters: {
|
||||||
|
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
//layout: 'centered',
|
||||||
|
},
|
||||||
|
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||||
|
tags: ['autodocs'],
|
||||||
|
title: 'Former/Controls Basic',
|
||||||
|
} satisfies Meta<typeof Renderable>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||||
|
export const BasicExample: Story = {
|
||||||
|
args: {
|
||||||
|
label: 'Test',
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user