Skip to content

Commit

Permalink
feat(form): Implemented Form Menu Item Components
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Apr 22, 2021
1 parent d9278b3 commit fed2b9f
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/form/src/index.ts
Expand Up @@ -6,6 +6,7 @@ export * from "./FormMessageContainer";

export * from "./file-input";
export * from "./label";
export * from "./menu";
export * from "./select";
export * from "./slider";
export * from "./text-field";
Expand Down
24 changes: 24 additions & 0 deletions packages/form/src/menu/MenuItemCheckbox.tsx
@@ -0,0 +1,24 @@
import React, { forwardRef } from "react";
import { useIcon } from "@react-md/icon";
import {
BaseMenuItemInputToggleProps,
MenuItemInputToggle,
} from "./MenuItemInputToggle";

/** @remarks \@since 2.8.0 */
export type MenuItemCheckboxProps = BaseMenuItemInputToggleProps;

/**
* This is a simple wrapper for the {@link MenuItemInputToggle} component to
* render it as a checkbox and pulling the checkbox icon from the
* {@link IconProvider}.
*/
export const MenuItemCheckbox = forwardRef<
HTMLLIElement,
MenuItemCheckboxProps
>(function MenuItemCheckbox({ icon: propIcon, ...props }, ref) {
const icon = useIcon("checkbox", propIcon);
return (
<MenuItemInputToggle {...props} ref={ref} icon={icon} type="checkbox" />
);
});
223 changes: 223 additions & 0 deletions packages/form/src/menu/MenuItemInputToggle.tsx
@@ -0,0 +1,223 @@
import React, {
forwardRef,
HTMLAttributes,
MouseEvent,
ReactNode,
} from "react";
import cn from "classnames";
import { useIcon } from "@react-md/icon";
import {
ListItem,
ListItemAddonPosition,
ListItemAddonType,
SimpleListItemProps,
} from "@react-md/list";
import { bem } from "@react-md/utils";
import { InputToggleIcon } from "../toggle/InputToggleIcon";
import { SwitchTrack } from "../toggle/SwitchTrack";

const styles = bem("rmd-input-toggle-menu-item");

/**
* @remarks \@since 2.8.0
* @internal
*/
type AllowedListItemProps = Pick<
SimpleListItemProps,
| "disabledOpacity"
| "threeLines"
| "height"
| "children"
| "textChildren"
| "textClassName"
| "primaryText"
| "secondaryText"
| "secondaryTextClassName"
| "forceAddonWrap"
>;

/** @remarks \@since 2.8.0 */
export interface BaseMenuItemInputToggleProps
extends HTMLAttributes<HTMLLIElement>,
AllowedListItemProps {
/**
* An id required for a11y.
*/
id: string;

/**
* Boolean if the element should be disabled.
*/
disabled?: boolean;

/**
* Boolean if the element is currently checked.
*/
checked: boolean;

/**
* A function to call that should updated the `checked` state to the new
* value.
*/
onCheckedChange(checked: boolean, event: MouseEvent<HTMLLIElement>): void;

/**
* The icon will default to:
* - {@link @react-md/icon#ConfigurableIcons.radio} when the `type` is set to
* `"radio"`
* - {@link @react-md/icon#ConfigurableIcons.checkbox} when the `type` is set
* to `"checkbox"`
* - {@link SwitchTrack} when the `type` is set to `"switch"`
*
* If this behavior isn't preferred, you can provide your own icon with this
* prop.
*/
icon?: ReactNode;

/**
* Boolean if the `icon` prop should appear as the `rightAddon` instead of the
* `leftAddon` for the `ListItem`
*/
iconAfter?: boolean;

/**
* An optional {@link @react-md/list#ListItem} addon to display on the
* opposite side of the `icon`. So if the `iconAfter` prop is `false`, the
* `addon` will appear to the `right` while setting `iconAfter` to `true` will
* render the `addon` to the `left` instead.
*/
addon?: ReactNode;

/**
* The {@link @react-md/list#ListItemAddonType} for the `addon`.
*/
addonType?: ListItemAddonType;

/**
* The {@link @react-md/list#ListItemAddonPosition} for the `addon`.
*/
addonPosition?: ListItemAddonPosition;
}

/** @remarks \@since 2.8.0 */
export interface MenuItemInputToggleProps extends BaseMenuItemInputToggleProps {
/**
* The input toggle type to render.
*/
type: "checkbox" | "radio" | "switch";
}

/**
* This is a low-level component that should probably not be used externally and
* instead the `MenuItemCheckbox`, `MenuItemRadio`, or `MenuItemSwitch` should
* be used instead.
*
* @remarks \@since 2.8.0
*/
export const MenuItemInputToggle = forwardRef<
HTMLLIElement,
MenuItemInputToggleProps
>(function MenuItemInputToggle(
{
children,
tabIndex = -1,
checked,
type,
icon: propIcon,
iconAfter = false,
addon,
addonType,
addonPosition,
onClick,
onCheckedChange,
disabled = false,
className,
...props
},
ref
) {
let icon = useIcon(type === "radio" ? "radio" : "checkbox", propIcon);
if (type === "switch" && typeof propIcon === "undefined") {
icon = <SwitchTrack checked={checked} />;
} else if (icon && type !== "switch") {
icon = (
<span className="rmd-toggle">
<InputToggleIcon
circle={type === "radio"}
disabled={disabled}
overlay
checked={checked}
>
{icon}
</InputToggleIcon>
</span>
);
}

let leftAddon: ReactNode;
let leftAddonType: ListItemAddonType | undefined;
let leftAddonPosition: ListItemAddonPosition | undefined;
let rightAddon: ReactNode;
let rightAddonType: ListItemAddonType | undefined;
let rightAddonPosition: ListItemAddonPosition | undefined;
if (iconAfter) {
leftAddon = addon;
leftAddonType = addonType;
leftAddonPosition = addonPosition;
rightAddon = icon;
} else {
leftAddon = icon;
rightAddon = addon;
rightAddonType = addonType;
rightAddonPosition = addonPosition;
}

return (
<ListItem
{...props}
disableRipple
aria-disabled={disabled || undefined}
aria-checked={checked}
role={type === "radio" ? "radio" : "menuitemcheckbox"}
onClick={(event) => {
onClick?.(event);
if (event.isPropagationStopped()) {
return;
}

onCheckedChange(!checked, event);
}}
ref={ref}
className={cn(styles(), className)}
tabIndex={tabIndex}
leftAddon={leftAddon}
leftAddonType={leftAddonType}
leftAddonPosition={leftAddonPosition}
rightAddon={rightAddon}
rightAddonType={rightAddonType}
rightAddonPosition={rightAddonPosition}
>
{children}
</ListItem>
);
});

/* istanbul ignore next */
if (process.env.NODE_ENV !== "production") {
try {
const PropTypes = require("prop-types");

MenuItemInputToggle.propTypes = {
id: PropTypes.string.isRequired,
checked: PropTypes.bool.isRequired,
onCheckedChange: PropTypes.func.isRequired,
type: PropTypes.oneOf(["checkbox", "radio", "switch"]).isRequired,
disabled: PropTypes.bool,
icon: PropTypes.node,
iconAfter: PropTypes.bool,
addon: PropTypes.node,
addonType: PropTypes.oneOf(["icon", "avatar", "media", "large-media"]),
addonPosition: PropTypes.oneOf(["top", "middle", "bottom"]),
};
} catch (e) {}
}
23 changes: 23 additions & 0 deletions packages/form/src/menu/MenuItemRadio.tsx
@@ -0,0 +1,23 @@
import React, { forwardRef } from "react";
import { useIcon } from "@react-md/icon";
import {
BaseMenuItemInputToggleProps,
MenuItemInputToggle,
} from "./MenuItemInputToggle";

/** @remarks \@since 2.8.0 */
export type MenuItemRadioProps = BaseMenuItemInputToggleProps;

/**
* This is a simple wrapper for the {@link MenuItemInputToggle} component to
* render it as a radio and pulling the radio icon from the
* {@link IconProvider}.
*/
export const MenuItemRadio = forwardRef<HTMLLIElement, MenuItemRadioProps>(
function MenuItemRadio({ icon: propIcon, ...props }, ref) {
const icon = useIcon("radio", propIcon);
return (
<MenuItemInputToggle {...props} ref={ref} icon={icon} type="radio" />
);
}
);
21 changes: 21 additions & 0 deletions packages/form/src/menu/MenuItemSwitch.tsx
@@ -0,0 +1,21 @@
import React, { forwardRef } from "react";

import {
BaseMenuItemInputToggleProps,
MenuItemInputToggle,
} from "./MenuItemInputToggle";

/** @remarks \@since 2.8.0 */
export type MenuItemSwitchProps = Omit<BaseMenuItemInputToggleProps, "icon">;

/**
* This is a simple wrapper for the {@link MenuItemInputToggle} component to
* render it as a switch.
*
* @remarks \@since 2.8.0
*/
export const MenuItemSwitch = forwardRef<HTMLLIElement, MenuItemSwitchProps>(
function MenuItemSwitch(props, ref) {
return <MenuItemInputToggle {...props} ref={ref} type="switch" />;
}
);
4 changes: 4 additions & 0 deletions packages/form/src/menu/index.ts
@@ -0,0 +1,4 @@
export * from "./MenuItemCheckbox";
export * from "./MenuItemRadio";
export * from "./MenuItemSwitch";
export * from "./MenuItemInputToggle";
5 changes: 5 additions & 0 deletions packages/form/src/toggle/_mixins.scss
Expand Up @@ -366,6 +366,11 @@
}
}

.rmd-input-toggle-menu-item {
// shrink the checkbox/radio icon size to just be an icon
@include rmd-button-theme-update-var(icon-size, rmd-icon-theme-var(size));
}

.rmd-switch-container {
@include rmd-switch-container;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/form/src/toggle/index.ts
@@ -1,7 +1,9 @@
export * from "./Checkbox";
export * from "./Radio";
export * from "./InputToggle";
export * from "./InputToggleIcon";
export * from "./ToggleContainer";
export * from "./Switch";
export * from "./SwitchTrack";
export * from "./AsyncSwitch";
export * from "./useChecked";

0 comments on commit fed2b9f

Please sign in to comment.