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