Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancement: add max-length, disabled to text and text_area #100

Merged
merged 4 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions frontend/src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { cn } from "@/lib/utils";

export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
icon?: React.ReactNode;
endAdornment?: React.ReactNode;
};

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
({ className, type, endAdornment, ...props }, ref) => {
const icon = props.icon;

return (
Expand All @@ -21,13 +22,20 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-6 w-full mb-1 rounded-sm shadow-xsSolid border border-input bg-transparent px-1.5 py-1 text-sm font-code ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground hover:shadow-smSolid focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-primary focus-visible:shadow-mdSolid disabled:cursor-not-allowed disabled:opacity-50",
"shadow-xsSolid hover:shadow-smSolid disabled:shadow-xsSolid focus-visible:shadow-mdSolid",
"flex h-6 w-full mb-1 rounded-sm border border-input bg-transparent px-1.5 py-1 text-sm font-code ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-primary disabled:cursor-not-allowed disabled:opacity-50",
icon && "pl-7",
endAdornment && "pr-10",
className
)}
ref={ref}
{...props}
/>
{endAdornment && (
<div className="absolute inset-y-0 right-0 flex items-center pr-[6px] pointer-events-none text-muted-foreground h-6">
{endAdornment}
</div>
)}
</div>
);
}
Expand Down
29 changes: 20 additions & 9 deletions frontend/src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,30 @@ import * as React from "react";

import { cn } from "@/lib/utils";

export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
bottomAdornment?: React.ReactNode;
}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
({ className, bottomAdornment, ...props }, ref) => {
return (
<textarea
className={cn(
"flex h-20 w-full mb-1 rounded-sm shadow-xsSolid border border-input bg-transparent px-3 py-2 text-sm font-code ring-offset-background placeholder:text-muted-foreground hover:shadow-smSolid focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-accent focus-visible:shadow-mdSolid disabled:cursor-not-allowed disabled:opacity-50",
className
<div className="relative">
<textarea
className={cn(
"shadow-xsSolid hover:shadow-smSolid disabled:shadow-xsSolid focus-visible:shadow-mdSolid",
"flex h-20 w-full mb-1 rounded-sm border border-input bg-transparent px-3 py-2 text-sm font-code ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-accent disabled:cursor-not-allowed disabled:opacity-50 min-h-[1.5rem]",
className
)}
ref={ref}
{...props}
/>
{bottomAdornment && (
<div className="absolute right-0 bottom-1 flex items-center pr-[6px] pointer-events-none text-muted-foreground h-6">
{bottomAdornment}
</div>
)}
ref={ref}
{...props}
/>
</div>
);
}
);
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/plugins/impl/TextAreaPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type T = string;
interface Data {
placeholder: string;
label: string | null;
maxLength?: number;
minLength?: number;
disabled?: boolean;
}

export class TextAreaPlugin implements IPlugin<T, Data> {
Expand All @@ -19,6 +22,9 @@ export class TextAreaPlugin implements IPlugin<T, Data> {
initialValue: z.string(),
placeholder: z.string(),
label: z.string().nullable(),
maxLength: z.number().optional(),
minLength: z.number().optional(),
disabled: z.boolean().optional(),
});

render(props: IPluginProps<T, Data>): JSX.Element {
Expand All @@ -38,12 +44,23 @@ interface TextAreaComponentProps extends Data {
}

const TextAreaComponent = (props: TextAreaComponentProps) => {
const bottomAdornment = props.maxLength ? (
<span className="text-muted-foreground text-xs font-medium">
{props.value.length}/{props.maxLength}
</span>
) : null;

return (
<Labeled label={props.label} align="top">
<Textarea
className="font-code"
rows={5}
cols={33}
maxLength={props.maxLength}
minLength={props.minLength}
required={props.minLength != null && props.minLength > 0}
disabled={props.disabled}
bottomAdornment={bottomAdornment}
value={props.value}
onInput={(event) =>
props.setValue((event.target as HTMLInputElement).value)
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/plugins/impl/TextInputPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ interface Data {
placeholder: string;
label: string | null;
kind: InputType;
maxLength?: number;
minLength?: number;
disabled?: boolean;
}

export class TextInputPlugin implements IPlugin<T, Data> {
Expand All @@ -26,6 +29,9 @@ export class TextInputPlugin implements IPlugin<T, Data> {
placeholder: z.string(),
label: z.string().nullable(),
kind: z.enum(["text", "password", "email", "url"]).default("text"),
maxLength: z.number().optional(),
minLength: z.number().optional(),
disabled: z.boolean().optional(),
});

render(props: IPluginProps<T, Data>): JSX.Element {
Expand Down Expand Up @@ -57,15 +63,26 @@ const TextComponent = (props: TextComponentProps) => {
url: <GlobeIcon size={16} />,
};

const endAdornment = props.maxLength ? (
<span className="text-muted-foreground text-xs font-medium">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change to a more visible color (like an amber or red) when the max length is hit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel that is usually if you go over, instead of exactly, and this will just stop at the max_length

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe amber isn't as "woah something is wrong"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, fine to keep as is for now. The only reason I suggested it is in case a user doesn't see the character count get confused why they can't continue typing

{props.value.length}/{props.maxLength}
</span>
) : null;

return (
<Labeled label={props.label}>
<Input
type={props.kind}
icon={icon[props.kind]}
placeholder={props.placeholder}
maxLength={props.maxLength}
minLength={props.minLength}
required={props.minLength != null && props.minLength > 0}
disabled={props.disabled}
className={cn({
"border-destructive": !isValid,
})}
endAdornment={endAdornment}
value={props.value}
onInput={(event) => props.setValue(event.currentTarget.value)}
onBlur={(event) => setValueOnBlur(event.currentTarget.value)}
Expand Down
12 changes: 12 additions & 0 deletions marimo/_plugins/ui/_impl/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ class text(UIElement[str, str]):
- `placeholder`: placeholder text to display when the text area is empty
- `kind`: input kind, one of `"text"`, `"password"`, `"email"`, or `"url"`
defaults to `"text"`
- `max_length`: maximum length of input
- `disabled`: whether the input is disabled
- `label`: text label for the element
- `on_change`: optional callback to run when this element's value changes
"""
Expand All @@ -337,6 +339,8 @@ def __init__(
value: str = "",
placeholder: str = "",
kind: Literal["text", "password", "email", "url"] = "text",
max_length: Optional[int] = None,
disabled: bool = False,
*,
label: str = "",
on_change: Optional[Callable[[str], None]] = None,
Expand All @@ -348,6 +352,8 @@ def __init__(
args={
"placeholder": placeholder,
"kind": kind,
"max-length": max_length,
"disabled": disabled,
},
on_change=on_change,
)
Expand Down Expand Up @@ -375,6 +381,8 @@ class text_area(UIElement[str, str]):

- `value`: initial value of the text area
- `placeholder`: placeholder text to display when the text area is empty
- `max_length`: maximum length of input
- `disabled`: whether the input is disabled
- `label`: text label for the element
- `on_change`: optional callback to run when this element's value changes
"""
Expand All @@ -385,6 +393,8 @@ def __init__(
self,
value: str = "",
placeholder: str = "",
max_length: Optional[int] = None,
disabled: bool = False,
*,
label: str = "",
on_change: Optional[Callable[[str], None]] = None,
Expand All @@ -395,6 +405,8 @@ def __init__(
label=label,
args={
"placeholder": placeholder,
"max-length": max_length,
"disabled": disabled,
},
on_change=on_change,
)
Expand Down
36 changes: 36 additions & 0 deletions marimo/_smoke_tests/inputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import marimo

__generated_with = "0.1.8"
app = marimo.App()


@app.cell
def __():
import marimo as mo
return mo,


@app.cell
def __(mo):
disabled = mo.ui.switch(label="Disabled")
mo.hstack([disabled])
return disabled,


@app.cell
def __(disabled, mo):
mo.vstack(
[
mo.ui.text(label="Your name", disabled=disabled.value),
mo.ui.text(
label="Your tagline", max_length=30, disabled=disabled.value
),
mo.ui.text_area(
label="Your bio", max_length=180, disabled=disabled.value
),
]
)
return

if __name__ == "__main__":
app.run()
Loading