Skip to content

Commit

Permalink
Add simple CycleButton and use it in three way checkboxes
Browse files Browse the repository at this point in the history
  • Loading branch information
Oksamies committed Jan 20, 2025
1 parent cb7e846 commit 9502128
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import {
faSquareXmark,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Checkbox from "@radix-ui/react-checkbox";

import styles from "./FilterMenu.module.css";
import { CategorySelection, CATEGORY_STATES as STATES } from "../types";
import { NewIcon } from "@thunderstore/cyberstorm";
import { CycleButton, NewIcon } from "@thunderstore/cyberstorm";
import { classnames } from "@thunderstore/cyberstorm/src/utils/utils";

interface Props {
Expand Down Expand Up @@ -42,28 +41,24 @@ export const CategoryMenu = (props: Props) => {
<li key={c.slug}>
<label className={classnames(styles.label, styles[c.selection])}>
{c.name}
<Checkbox.Root
checked={c.selection !== "off"}
onCheckedChange={() => toggleCategory(c.id)}
className={styles.checkbox}
<CycleButton
onInterract={() => toggleCategory(c.id)}
rootClasses={styles.checkbox}
value={c.selection}
noState
>
{c.selection === "off" ? (
<NewIcon csMode="inline" noWrapper>
<FontAwesomeIcon icon={faSquare} />
</NewIcon>
) : null}
<Checkbox.Indicator asChild>
<NewIcon csMode="inline" noWrapper>
<FontAwesomeIcon
icon={
c.selection === "include"
? faSquareCheck
: faSquareXmark
}
/>
</NewIcon>
</Checkbox.Indicator>
</Checkbox.Root>
<NewIcon csMode="inline" noWrapper>
<FontAwesomeIcon
icon={
c.selection === "include"
? faSquareCheck
: c.selection === "exclude"
? faSquareXmark
: faSquare
}
/>
</NewIcon>
</CycleButton>
</label>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { faSquare, faSquareCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Checkbox from "@radix-ui/react-checkbox";

import styles from "./FilterMenu.module.css";
import { NewIcon } from "@thunderstore/cyberstorm";
import { CycleButton, NewIcon } from "@thunderstore/cyberstorm";
import { classnames } from "@thunderstore/cyberstorm/src/utils/utils";

interface Props {
Expand Down Expand Up @@ -33,15 +32,16 @@ export const OthersMenu = (props: Props) => {
)}
>
{label}
<Checkbox.Root
checked={checked}
onCheckedChange={() => setChecked(!checked)}
className={styles.checkbox}
<CycleButton
onInterract={() => setChecked(!checked)}
rootClasses={styles.checkbox}
value={checked ? "on" : "off"}
noState
>
<NewIcon csMode="inline" noWrapper>
<FontAwesomeIcon icon={checked ? faSquareCheck : faSquare} />
</NewIcon>
</Checkbox.Root>
</CycleButton>
</label>
</li>
))}
Expand Down
1 change: 1 addition & 0 deletions packages/cyberstorm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export { CardCommunity } from "./newComponents/Card/CardCommunity/CardCommunity"
export { CardPackage } from "./newComponents/Card/CardPackage/CardPackage";
export { Link as NewLink } from "./newComponents/Link/Link/Link";
export { Button as NewButton } from "./newComponents/Button/Button";
export { CycleButton } from "./newComponents/CycleButton/CycleButton";
export { BreadCrumbs as NewBreadCrumbs } from "./newComponents/BreadCrumbs/BreadCrumbs";
export { TextInput as NewTextInput } from "./newComponents/TextInput/TextInput";
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@layer components {
.ts-cyclebutton {
display: inline-flex;
align-items: center;
}
}
85 changes: 85 additions & 0 deletions packages/cyberstorm/src/newComponents/CycleButton/CycleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import "./CycleButton.css";
import React, { useState } from "react";
import { classnames } from "../../utils/utils";
import {
Actionable,
ActionableButtonProps,
} from "../../primitiveComponents/Actionable/Actionable";

interface CycleButtonProps
extends Omit<ActionableButtonProps, "primitiveType"> {
noState?: boolean;
value?: string;
onInterract?: () => void;
options?: string[];
onValueChange?: (value: string) => void;
}

/**
* Button for cycling through values
* If you want to handle state outside of this component use the "noState" param
* Notes for handling state inside this component:
* - First value of the list will be the initial value
* - You can access the value from onValueChange. It's called each time value changes.
*/
export const CycleButton = React.forwardRef<
HTMLButtonElement,
CycleButtonProps
>((props: CycleButtonProps, forwardedRef) => {
const {
children,
rootClasses,
noState = false,
onInterract,
options,
onValueChange,
...forwardedProps
} = props;

if (noState) {
return (
<Actionable
{...forwardedProps}
primitiveType={"button"}
rootClasses={classnames("ts-cyclebutton", rootClasses)}
ref={forwardedRef}
onClick={onInterract ? () => onInterract() : undefined}
>
{children}
</Actionable>
);
}

const initialValue = options && options.length > 0 ? options[0] : "";
const [currentValue, setCurrentValue] = useState<string>(initialValue);

return (
<Actionable
{...forwardedProps}
primitiveType={"button"}
rootClasses={classnames("ts-cyclebutton", rootClasses)}
ref={forwardedRef}
onClick={() => {
if (options && options.length > 0) {
const currentValueIndex = options?.indexOf(currentValue);
if (
currentValueIndex === -1 ||
currentValueIndex === options.length - 1
) {
setCurrentValue(options[0]);
} else {
setCurrentValue(options[currentValueIndex + 1]);
}
}
if (onInterract) {
onInterract();
}
}}
onChange={onValueChange ? () => onValueChange(currentValue) : undefined}
>
{children}
</Actionable>
);
});

CycleButton.displayName = "CycleButton";

0 comments on commit 9502128

Please sign in to comment.