Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
FieldLabel,
FieldError,
inputFieldStyles,
useRootContainer,
POPOVER_LIST_BOX_MAX_HEIGHT,
} from "@appsmith/wds";
import React from "react";
import { ComboBox as HeadlessCombobox } from "react-aria-components";

import styles from "./styles.module.css";
import type { ComboBoxProps } from "./types";
import { ComboBoxTrigger } from "./ComboBoxTrigger";

Expand All @@ -24,9 +27,7 @@ export const ComboBox = (props: ComboBoxProps) => {
size = "medium",
...rest
} = props;
const root = document.body.querySelector(
"[data-theme-provider]",
) as HTMLButtonElement;
const root = useRootContainer();

return (
<HeadlessCombobox
Expand All @@ -51,7 +52,11 @@ export const ComboBox = (props: ComboBoxProps) => {
size={size}
/>
<FieldError>{errorMessage}</FieldError>
<Popover UNSTABLE_portalContainer={root}>
<Popover
UNSTABLE_portalContainer={root}
className={styles.comboboxPopover}
maxHeight={POPOVER_LIST_BOX_MAX_HEIGHT}
>
<ListBox shouldFocusWrap>{children}</ListBox>
</Popover>
</HeadlessCombobox>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import clsx from "clsx";
import React, { useMemo } from "react";
import { getTypographyClassName } from "@appsmith/wds-theming";
import { Spinner, textInputStyles, Input, IconButton } from "@appsmith/wds";
import { Spinner, Input, IconButton } from "@appsmith/wds";

import type { ComboBoxProps } from "./types";

Expand All @@ -27,12 +25,5 @@ export const ComboBoxTrigger: React.FC<ComboBoxTriggerProps> = (props) => {
);
}, [isLoading, size, isDisabled]);

return (
<Input
className={clsx(textInputStyles.input, getTypographyClassName("body"))}
placeholder={placeholder}
size={size}
suffix={suffix}
/>
);
return <Input placeholder={placeholder} size={size} suffix={suffix} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.comboboxPopover {
width: calc(var(--trigger-width) + var(--inner-spacing-2) * 2);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we change how the component is styled, these styles are need so that popover takes full width.

transform: translateX(calc(-1 * var(--inner-spacing-1)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Calendar,
inputFieldStyles,
TimeField,
useRootContainer,
} from "@appsmith/wds";
import clsx from "clsx";
import React from "react";
Expand Down Expand Up @@ -44,6 +45,7 @@ export const DatePicker = <T extends DateValue>(props: DatePickerProps<T>) => {
const timeMaxValue = (
props.maxValue && "hour" in props.maxValue ? props.maxValue : null
) as TimeValue;
const root = useRootContainer();

return (
<HeadlessDatePicker
Expand All @@ -55,9 +57,6 @@ export const DatePicker = <T extends DateValue>(props: DatePickerProps<T>) => {
{...rest}
>
{({ state }) => {
const root = document.body.querySelector(
"[data-theme-provider]",
) as HTMLButtonElement;
const timeGranularity =
state.granularity === "hour" ||
state.granularity === "minute" ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import clsx from "clsx";
import React, { forwardRef, useState } from "react";
import { mergeRefs } from "@react-aria/utils";
import React, { forwardRef, useRef, useState } from "react";
import { getTypographyClassName } from "@appsmith/wds-theming";
import { IconButton, Spinner, type IconProps } from "@appsmith/wds";
import { Group, Input as HeadlessInput } from "react-aria-components";
Expand All @@ -9,6 +10,7 @@ import type { InputProps } from "./types";

function _Input(props: InputProps, ref: React.Ref<HTMLInputElement>) {
const {
className,
defaultValue,
isLoading,
isReadOnly,
Expand All @@ -19,6 +21,8 @@ function _Input(props: InputProps, ref: React.Ref<HTMLInputElement>) {
value,
...rest
} = props;
const localRef = useRef<HTMLInputElement>(null);
const mergedRef = mergeRefs(ref, localRef);
const [showPassword, setShowPassword] = useState(false);
const togglePasswordVisibility = () => setShowPassword((prev) => !prev);
const isEmpty = !Boolean(value) && !Boolean(defaultValue);
Expand Down Expand Up @@ -46,18 +50,30 @@ function _Input(props: InputProps, ref: React.Ref<HTMLInputElement>) {

return (
<Group className={styles.inputGroup}>
{Boolean(prefix) && (
<span data-input-prefix onClick={() => localRef.current?.focus()}>
{prefix}
</span>
)}
<HeadlessInput
{...rest}
className={clsx(styles.input, getTypographyClassName("body"))}
className={clsx(
styles.input,
getTypographyClassName("body"),
className,
)}
data-readonly={Boolean(isReadOnly) ? true : undefined}
data-size={Boolean(size) ? size : undefined}
defaultValue={defaultValue}
ref={ref}
ref={mergedRef}
type={showPassword ? "text" : type}
value={isEmpty && Boolean(isReadOnly) ? "—" : value}
/>
{Boolean(prefix) && <span data-input-prefix>{prefix}</span>}
{Boolean(suffix) && <span data-input-suffix>{suffix}</span>}
{Boolean(suffix) && (
<span data-input-suffix onClick={() => localRef.current?.focus()}>
{suffix}
</span>
)}
</Group>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@
width: 100%;
}

.input {
position: relative;
display: flex;
flex: 1;
align-items: center;
box-sizing: content-box;
.inputGroup {
max-inline-size: 100%;
padding-block: var(--inner-spacing-1);
padding-inline: var(--inner-spacing-2);
Expand All @@ -25,8 +20,12 @@
overflow: hidden;
}

.input:has(> [data-select-text]) {
block-size: var(--body-line-height);
.input {
border: none;
outline: none;
background-color: transparent;
flex: 1;
padding: 0;
}

.input:is(textarea) {
Expand Down Expand Up @@ -57,59 +56,27 @@
);
}

.inputGroup:has(> [data-input-prefix]) .input {
padding-left: var(--sizing-8);
}

.inputGroup:has(> [data-input-prefix]) .input[data-size="large"] {
padding-left: var(--sizing-12);
}

.inputGroup:has(> [data-input-prefix]) .input[data-size="small"] {
padding-left: var(--sizing-6);
}

.inputGroup:has(> [data-input-prefix]) [data-input-prefix] {
left: var(--inner-spacing-1);
position: absolute;
}

.inputGroup:has(> [data-input-suffix]) .input {
padding-right: var(--sizing-8);
}

.inputGroup:has(> [data-input-suffix]) .input[data-size="large"] {
padding-right: var(--sizing-12);
}

.inputGroup:has(> [data-input-suffix]) .input[data-size="small"] {
padding-right: var(--sizing-6);
}

.inputGroup:has(> [data-input-suffix]) [data-input-suffix] {
right: var(--inner-spacing-1);
position: absolute;
}

.inputGroup :is([data-input-suffix], [data-input-prefix]) {
display: flex;
justify-content: center;
align-items: center;
height: 0;
}

/**
* ----------------------------------------------------------------------------
* HOVERED
* ----------------------------------------------------------------------------
*/
.inputGroup[data-hovered]
.input:not(
:is(
[data-focused],
[data-readonly],
[data-disabled],
[data-focus-within],
:has(~ input[data-disabled="true"])
.inputGroup[data-hovered]:has(
> .input:not(
:is(
[data-focused],
[data-readonly],
[data-disabled],
[data-focus-within],
:has(~ input[data-disabled="true"])
)
)
) {
background-color: var(--color-bg-neutral-subtle-hover);
Expand Down Expand Up @@ -157,8 +124,8 @@
* DISABLED
* ----------------------------------------------------------------------------
*/
.input[data-disabled],
.input:has(~ input[data-disabled]) {
.inputGroup:has(> .input[data-disabled]),
.inputGroup:has(> .input:has(~ input[data-disabled])) {
cursor: not-allowed;
box-shadow: none;
}
Expand All @@ -168,13 +135,14 @@
* INVALID
* ----------------------------------------------------------------------------
*/
.input[data-invalid] {
.inputGroup:has(> .input[data-invalid]) {
box-shadow: 0 0 0 1px var(--color-bd-negative);
}

.inputGroup[data-hovered]
.input[data-invalid]:not(
:is([data-focused], [data-readonly], [data-disabled])
.inputGroup[data-hovered]:has(
> .input[data-invalid]:not(
:is([data-focused], [data-readonly], [data-disabled])
)
) {
box-shadow: 0 0 0 1px var(--color-bd-negative-hover);
}
Expand All @@ -184,7 +152,9 @@
* FOCUSSED
* ----------------------------------------------------------------------------
*/
.input:is([data-focused], [data-focus-within]):not([data-readonly]) {
.inputGroup:has(
> .input:is([data-focused], [data-focus-within]):not([data-readonly])
) {
background-color: transparent;
box-shadow: 0 0 0 2px var(--color-bd-focus);
}
Expand All @@ -194,31 +164,17 @@
* SIZE
* ----------------------------------------------------------------------------
*/
.input[data-size="small"] {
.inputGroup:has(> .input[data-size="small"]) {
block-size: calc(
var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end)
var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) +
var(--inner-spacing-2) * 2
);
padding-block: var(--inner-spacing-2);
}

.input[data-size="large"] {
.inputGroup:has(> .input[data-size="large"]) {
block-size: calc(
var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end)
);
padding-block: var(--inner-spacing-3);
padding-inline: var(--inner-spacing-3);
}

/**
* ----------------------------------------------------------------------------
* SELECT BUTTON's TEXT
* ----------------------------------------------------------------------------
*/
.input [data-select-text] {
display: flex;
align-items: center;
}

.input [data-select-text] [data-icon] {
margin-inline-end: var(--inner-spacing-1);
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import {
Popover,
listStyles,
useRootContainer,
POPOVER_LIST_BOX_MAX_HEIGHT,
} from "@appsmith/wds";
import React, { createContext, useContext } from "react";
import { listStyles, Popover } from "@appsmith/wds";
import { Menu as HeadlessMenu } from "react-aria-components";

import type { MenuProps } from "./types";
import clsx from "clsx";

const MenuNestingContext = createContext(0);

export const Menu = (props: MenuProps) => {
const { children } = props;
const root = document.body.querySelector(
"[data-theme-provider]",
) as HTMLButtonElement;
const { children, className, ...rest } = props;
const root = useRootContainer();

const nestingLevel = useContext(MenuNestingContext);
const isRootMenu = nestingLevel === 0;

return (
<MenuNestingContext.Provider value={nestingLevel + 1}>
{/* Only the parent Popover should be placed in the root. Placing child popoves in root would cause the menu to function incorrectly */}
<Popover UNSTABLE_portalContainer={isRootMenu ? root : undefined}>
<HeadlessMenu className={listStyles.listBox} {...props}>
<Popover
UNSTABLE_portalContainer={isRootMenu ? root : undefined}
maxHeight={POPOVER_LIST_BOX_MAX_HEIGHT}
>
<HeadlessMenu className={clsx(listStyles.listBox, className)} {...rest}>
{children}
</HeadlessMenu>
</Popover>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import type { MenuProps as AriaMenuProps } from "react-aria-components";

export interface MenuProps
extends Omit<
AriaMenuProps<object>,
"slot" | "selectionMode" | "selectedKeys"
> {}
export interface MenuProps extends Omit<AriaMenuProps<object>, "slot"> {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import {
import { Icon, Text, listBoxItemStyles } from "@appsmith/wds";

import type { MenuItemProps } from "./types";
import clsx from "clsx";

export function MenuItem(props: MenuItemProps) {
const { children, icon, ...rest } = props;
const { children, className, icon, ...rest } = props;

return (
<HeadlessMenuItem {...rest} className={listBoxItemStyles.listBoxItem}>
<HeadlessMenuItem
{...rest}
className={clsx(listBoxItemStyles.listBoxItem, className)}
>
{composeRenderProps(children, (children, { hasSubmenu }) => (
<>
{icon && <Icon name={icon} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const POPOVER_LIST_BOX_MAX_HEIGHT = 400;
Loading