Skip to content

Commit fed2b9f

Browse files
committed
feat(form): Implemented Form Menu Item Components
1 parent d9278b3 commit fed2b9f

File tree

8 files changed

+303
-0
lines changed

8 files changed

+303
-0
lines changed

packages/form/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from "./FormMessageContainer";
66

77
export * from "./file-input";
88
export * from "./label";
9+
export * from "./menu";
910
export * from "./select";
1011
export * from "./slider";
1112
export * from "./text-field";
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React, { forwardRef } from "react";
2+
import { useIcon } from "@react-md/icon";
3+
import {
4+
BaseMenuItemInputToggleProps,
5+
MenuItemInputToggle,
6+
} from "./MenuItemInputToggle";
7+
8+
/** @remarks \@since 2.8.0 */
9+
export type MenuItemCheckboxProps = BaseMenuItemInputToggleProps;
10+
11+
/**
12+
* This is a simple wrapper for the {@link MenuItemInputToggle} component to
13+
* render it as a checkbox and pulling the checkbox icon from the
14+
* {@link IconProvider}.
15+
*/
16+
export const MenuItemCheckbox = forwardRef<
17+
HTMLLIElement,
18+
MenuItemCheckboxProps
19+
>(function MenuItemCheckbox({ icon: propIcon, ...props }, ref) {
20+
const icon = useIcon("checkbox", propIcon);
21+
return (
22+
<MenuItemInputToggle {...props} ref={ref} icon={icon} type="checkbox" />
23+
);
24+
});
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import React, {
2+
forwardRef,
3+
HTMLAttributes,
4+
MouseEvent,
5+
ReactNode,
6+
} from "react";
7+
import cn from "classnames";
8+
import { useIcon } from "@react-md/icon";
9+
import {
10+
ListItem,
11+
ListItemAddonPosition,
12+
ListItemAddonType,
13+
SimpleListItemProps,
14+
} from "@react-md/list";
15+
import { bem } from "@react-md/utils";
16+
import { InputToggleIcon } from "../toggle/InputToggleIcon";
17+
import { SwitchTrack } from "../toggle/SwitchTrack";
18+
19+
const styles = bem("rmd-input-toggle-menu-item");
20+
21+
/**
22+
* @remarks \@since 2.8.0
23+
* @internal
24+
*/
25+
type AllowedListItemProps = Pick<
26+
SimpleListItemProps,
27+
| "disabledOpacity"
28+
| "threeLines"
29+
| "height"
30+
| "children"
31+
| "textChildren"
32+
| "textClassName"
33+
| "primaryText"
34+
| "secondaryText"
35+
| "secondaryTextClassName"
36+
| "forceAddonWrap"
37+
>;
38+
39+
/** @remarks \@since 2.8.0 */
40+
export interface BaseMenuItemInputToggleProps
41+
extends HTMLAttributes<HTMLLIElement>,
42+
AllowedListItemProps {
43+
/**
44+
* An id required for a11y.
45+
*/
46+
id: string;
47+
48+
/**
49+
* Boolean if the element should be disabled.
50+
*/
51+
disabled?: boolean;
52+
53+
/**
54+
* Boolean if the element is currently checked.
55+
*/
56+
checked: boolean;
57+
58+
/**
59+
* A function to call that should updated the `checked` state to the new
60+
* value.
61+
*/
62+
onCheckedChange(checked: boolean, event: MouseEvent<HTMLLIElement>): void;
63+
64+
/**
65+
* The icon will default to:
66+
* - {@link @react-md/icon#ConfigurableIcons.radio} when the `type` is set to
67+
* `"radio"`
68+
* - {@link @react-md/icon#ConfigurableIcons.checkbox} when the `type` is set
69+
* to `"checkbox"`
70+
* - {@link SwitchTrack} when the `type` is set to `"switch"`
71+
*
72+
* If this behavior isn't preferred, you can provide your own icon with this
73+
* prop.
74+
*/
75+
icon?: ReactNode;
76+
77+
/**
78+
* Boolean if the `icon` prop should appear as the `rightAddon` instead of the
79+
* `leftAddon` for the `ListItem`
80+
*/
81+
iconAfter?: boolean;
82+
83+
/**
84+
* An optional {@link @react-md/list#ListItem} addon to display on the
85+
* opposite side of the `icon`. So if the `iconAfter` prop is `false`, the
86+
* `addon` will appear to the `right` while setting `iconAfter` to `true` will
87+
* render the `addon` to the `left` instead.
88+
*/
89+
addon?: ReactNode;
90+
91+
/**
92+
* The {@link @react-md/list#ListItemAddonType} for the `addon`.
93+
*/
94+
addonType?: ListItemAddonType;
95+
96+
/**
97+
* The {@link @react-md/list#ListItemAddonPosition} for the `addon`.
98+
*/
99+
addonPosition?: ListItemAddonPosition;
100+
}
101+
102+
/** @remarks \@since 2.8.0 */
103+
export interface MenuItemInputToggleProps extends BaseMenuItemInputToggleProps {
104+
/**
105+
* The input toggle type to render.
106+
*/
107+
type: "checkbox" | "radio" | "switch";
108+
}
109+
110+
/**
111+
* This is a low-level component that should probably not be used externally and
112+
* instead the `MenuItemCheckbox`, `MenuItemRadio`, or `MenuItemSwitch` should
113+
* be used instead.
114+
*
115+
* @remarks \@since 2.8.0
116+
*/
117+
export const MenuItemInputToggle = forwardRef<
118+
HTMLLIElement,
119+
MenuItemInputToggleProps
120+
>(function MenuItemInputToggle(
121+
{
122+
children,
123+
tabIndex = -1,
124+
checked,
125+
type,
126+
icon: propIcon,
127+
iconAfter = false,
128+
addon,
129+
addonType,
130+
addonPosition,
131+
onClick,
132+
onCheckedChange,
133+
disabled = false,
134+
className,
135+
...props
136+
},
137+
ref
138+
) {
139+
let icon = useIcon(type === "radio" ? "radio" : "checkbox", propIcon);
140+
if (type === "switch" && typeof propIcon === "undefined") {
141+
icon = <SwitchTrack checked={checked} />;
142+
} else if (icon && type !== "switch") {
143+
icon = (
144+
<span className="rmd-toggle">
145+
<InputToggleIcon
146+
circle={type === "radio"}
147+
disabled={disabled}
148+
overlay
149+
checked={checked}
150+
>
151+
{icon}
152+
</InputToggleIcon>
153+
</span>
154+
);
155+
}
156+
157+
let leftAddon: ReactNode;
158+
let leftAddonType: ListItemAddonType | undefined;
159+
let leftAddonPosition: ListItemAddonPosition | undefined;
160+
let rightAddon: ReactNode;
161+
let rightAddonType: ListItemAddonType | undefined;
162+
let rightAddonPosition: ListItemAddonPosition | undefined;
163+
if (iconAfter) {
164+
leftAddon = addon;
165+
leftAddonType = addonType;
166+
leftAddonPosition = addonPosition;
167+
rightAddon = icon;
168+
} else {
169+
leftAddon = icon;
170+
rightAddon = addon;
171+
rightAddonType = addonType;
172+
rightAddonPosition = addonPosition;
173+
}
174+
175+
return (
176+
<ListItem
177+
{...props}
178+
disableRipple
179+
aria-disabled={disabled || undefined}
180+
aria-checked={checked}
181+
role={type === "radio" ? "radio" : "menuitemcheckbox"}
182+
onClick={(event) => {
183+
onClick?.(event);
184+
if (event.isPropagationStopped()) {
185+
return;
186+
}
187+
188+
onCheckedChange(!checked, event);
189+
}}
190+
ref={ref}
191+
className={cn(styles(), className)}
192+
tabIndex={tabIndex}
193+
leftAddon={leftAddon}
194+
leftAddonType={leftAddonType}
195+
leftAddonPosition={leftAddonPosition}
196+
rightAddon={rightAddon}
197+
rightAddonType={rightAddonType}
198+
rightAddonPosition={rightAddonPosition}
199+
>
200+
{children}
201+
</ListItem>
202+
);
203+
});
204+
205+
/* istanbul ignore next */
206+
if (process.env.NODE_ENV !== "production") {
207+
try {
208+
const PropTypes = require("prop-types");
209+
210+
MenuItemInputToggle.propTypes = {
211+
id: PropTypes.string.isRequired,
212+
checked: PropTypes.bool.isRequired,
213+
onCheckedChange: PropTypes.func.isRequired,
214+
type: PropTypes.oneOf(["checkbox", "radio", "switch"]).isRequired,
215+
disabled: PropTypes.bool,
216+
icon: PropTypes.node,
217+
iconAfter: PropTypes.bool,
218+
addon: PropTypes.node,
219+
addonType: PropTypes.oneOf(["icon", "avatar", "media", "large-media"]),
220+
addonPosition: PropTypes.oneOf(["top", "middle", "bottom"]),
221+
};
222+
} catch (e) {}
223+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React, { forwardRef } from "react";
2+
import { useIcon } from "@react-md/icon";
3+
import {
4+
BaseMenuItemInputToggleProps,
5+
MenuItemInputToggle,
6+
} from "./MenuItemInputToggle";
7+
8+
/** @remarks \@since 2.8.0 */
9+
export type MenuItemRadioProps = BaseMenuItemInputToggleProps;
10+
11+
/**
12+
* This is a simple wrapper for the {@link MenuItemInputToggle} component to
13+
* render it as a radio and pulling the radio icon from the
14+
* {@link IconProvider}.
15+
*/
16+
export const MenuItemRadio = forwardRef<HTMLLIElement, MenuItemRadioProps>(
17+
function MenuItemRadio({ icon: propIcon, ...props }, ref) {
18+
const icon = useIcon("radio", propIcon);
19+
return (
20+
<MenuItemInputToggle {...props} ref={ref} icon={icon} type="radio" />
21+
);
22+
}
23+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { forwardRef } from "react";
2+
3+
import {
4+
BaseMenuItemInputToggleProps,
5+
MenuItemInputToggle,
6+
} from "./MenuItemInputToggle";
7+
8+
/** @remarks \@since 2.8.0 */
9+
export type MenuItemSwitchProps = Omit<BaseMenuItemInputToggleProps, "icon">;
10+
11+
/**
12+
* This is a simple wrapper for the {@link MenuItemInputToggle} component to
13+
* render it as a switch.
14+
*
15+
* @remarks \@since 2.8.0
16+
*/
17+
export const MenuItemSwitch = forwardRef<HTMLLIElement, MenuItemSwitchProps>(
18+
function MenuItemSwitch(props, ref) {
19+
return <MenuItemInputToggle {...props} ref={ref} type="switch" />;
20+
}
21+
);

packages/form/src/menu/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./MenuItemCheckbox";
2+
export * from "./MenuItemRadio";
3+
export * from "./MenuItemSwitch";
4+
export * from "./MenuItemInputToggle";

packages/form/src/toggle/_mixins.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,11 @@
366366
}
367367
}
368368

369+
.rmd-input-toggle-menu-item {
370+
// shrink the checkbox/radio icon size to just be an icon
371+
@include rmd-button-theme-update-var(icon-size, rmd-icon-theme-var(size));
372+
}
373+
369374
.rmd-switch-container {
370375
@include rmd-switch-container;
371376
}

packages/form/src/toggle/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export * from "./Checkbox";
22
export * from "./Radio";
33
export * from "./InputToggle";
4+
export * from "./InputToggleIcon";
45
export * from "./ToggleContainer";
56
export * from "./Switch";
7+
export * from "./SwitchTrack";
68
export * from "./AsyncSwitch";
79
export * from "./useChecked";

0 commit comments

Comments
 (0)