Skip to content

Commit

Permalink
Merge pull request #5 from thep0y/main
Browse files Browse the repository at this point in the history
0.1.2
  • Loading branch information
thep0y authored Dec 4, 2024
2 parents 88d2411 + 8758e5c commit 68b3a0b
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fluent-solid",
"version": "0.1.1",
"version": "0.1.2",
"repository": "https://github.com/thep0y/fluent",
"keywords": [
"solid",
Expand Down
107 changes: 107 additions & 0 deletions packages/components/switch/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
.fluent-switch {
align-items: flex-start;
box-sizing: border-box;
display: inline-flex;
position: relative;

&__input {
top: 0;
left: 0;
box-sizing: border-box;
cursor: pointer;
height: 100%;
margin: 0px;
opacity: 0;
position: absolute;
width: calc(40px + 2 * var(--spacingHorizontalS));

&:checked ~ .fluent-switch__indicator > * {
transform: translateX(20px);
}

&:enabled:checked ~ .fluent-switch__indicator {
background-color: var(--colorCompoundBrandBackground);
color: var(--colorNeutralForegroundInverted);
border-color: var(--colorTransparentStroke);
}

&:enabled:checked:hover:active ~ .fluent-switch__indicator {
background-color: var(--colorCompoundBrandBackgroundPressed);
border-color: var(--colorTransparentStrokeInteractive);
}

&:enabled:checked:hover ~ .fluent-switch__indicator {
background-color: var(--colorCompoundBrandBackgroundPressed);
border-color: var(--colorTransparentStrokeInteractive);
}

&:enabled:checked ~ .fluent-switch__indicator {
background-color: var(--colorCompoundBrandBackgroundPressed);
border-color: var(--colorTransparentStrokeInteractive);
}

&:disabled {
cursor: default;
}

&:disabled:not(:checked) ~ .fluent-switch__indicator {
border-color: var(--colorNeutralStrokeDisabled);
}

&:disabled:checked ~ .fluent-switch__indicator {
background-color: var(--colorNeutralBackgroundDisabled);
border-color: var(--colorTransparentStrokeDisabled);
}

&:disabled ~ .fluent-switch__indicator {
color: var(--colorNeutralForegroundDisabled);
}

&:disabled ~ .fluent-switch__label {
cursor: default;
color: var(--colorNeutralForegroundDisabled);
}
}

&__label {
margin-top: calc((20px - var(--lineHeightBase300)) / 2);
margin-bottom: calc((20px - var(--lineHeightBase300)) / 2);
cursor: pointer;
padding-left: var(--spacingHorizontalXS);
padding: var(--spacingVerticalS) var(--spacingHorizontalS);
}

&__indicator {
color: var(--colorNeutralStrokeAccessible);
border-color: var(--colorNeutralStrokeAccessible);
border-radius: var(--borderRadiusCircular);
border: 1px solid;
line-height: 0;
box-sizing: border-box;
fill: currentcolor;
flex-shrink: 0;
font-size: 18px;
height: 20px;
margin: var(--spacingVerticalS) var(--spacingHorizontalS);
pointer-events: none;
transition-duration: var(--durationNormal);
transition-timing-function: var(--curveEasyEase);
transition-property: background, border, color;
width: 40px;

& > svg {
transition-duration: var(--durationNormal);
transition-timing-function: var(--curveEasyEase);
transition-property: transform;
}

& > svg {
display: inline;
line-height: 0;
}
}

&-label-above {
flex-direction: column;
}
}
146 changes: 146 additions & 0 deletions packages/components/switch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { addClassList } from "~/utils";
import "./index.scss";
import type {
BaseNoChildrenComponentProps,
HTMLDivElementProps,
HTMLInputElementProps,
} from "~/interface";
import { children, createSignal, lazy, mergeProps } from "solid-js";
import { BiSolidCircle } from "solid-icons/bi";
import type { JSX } from "solid-js";

const LazyLabel = lazy(() => import("~/components/label"));

interface SwitchProps extends BaseNoChildrenComponentProps<HTMLDivElement> {
/**
* Defines the controlled checked state of the Switch.
* If passed, Switch ignores the `defaultChecked` property.
* This should only be used if the checked state is to be controlled at a higher level and there is a plan to pass the
* correct value based on handling `onChange` events and re-rendering.
*
* @default false
*/
checked?: boolean;

/**
* Defines whether the Switch is initially in a checked state or not when rendered.
*
* @default false
*/
defaultChecked?: boolean;

/**
* The position of the label relative to the Switch.
*
* @default after
*/
labelPosition?: "above" | "after" | "before";

/**
* Callback to be called when the checked state value changes.
*/
onChange?: (checked: boolean) => void;

/**
* The track and the thumb sliding over it indicating the on and off status of the Switch.
*/
indicator?: JSX.Element;

/**
* Hidden input that handles the Switch's functionality.
*
* This is the PRIMARY slot: all native properties specified directly on the `<Switch>` tag will be applied to this
* slot, except `className` and `style`, which remain on the root slot.
*/
//input: HTMLInputElement;

/**
* The Switch's label.
*/
label?: string;

disabled?: boolean;
required?: boolean;
}

const baseClassName = "fluent-switch";

const Switch = (props: SwitchProps) => {
const merged = mergeProps(
{ indicator: <BiSolidCircle />, labelPosition: "after" },
props,
);

const [isChecked, setIsChecked] = createSignal(
merged.checked ?? merged.defaultChecked ?? false,
);

const handleChange: HTMLInputElementProps["onChange"] = (event) => {
const checked = event.currentTarget.checked;

if (merged.checked === undefined) {
setIsChecked(checked);
}

merged.onChange?.(checked);
};

const classList = () =>
addClassList({
base: baseClassName,
others: {
[`${baseClassName}-label-${merged.labelPosition}`]:
merged.label && merged.labelPosition,
[`${baseClassName}-disabled`]: merged.disabled,
[`${merged.class}`]: merged.class,
},
});

const onClick: HTMLDivElementProps["onClick"] = (event) => {
if (merged.disabled) return;

const target = event.target;
if (target.className === `${baseClassName}__input`) {
return;
}
const input: HTMLInputElement = event.currentTarget.querySelector(
`.${baseClassName}__input`,
)!;
input.click();
};

const renderLabel = children(() => {
if (!merged.label) return null;
return (
<LazyLabel class={`${baseClassName}__label`} required={merged.required}>
{merged.label}
</LazyLabel>
);
});

return (
<div classList={classList()} onClick={onClick}>
<input
class={`${baseClassName}__input`}
role="switch"
type="checkbox"
checked={merged.checked ?? isChecked()}
aria-checked={merged.checked ?? isChecked()}
onChange={handleChange}
disabled={merged.disabled}
/>

{(merged.labelPosition === "before" ||
merged.labelPosition === "above") &&
renderLabel()}

<div class={`${baseClassName}__indicator`} aria-hidden={true}>
{merged.indicator}
</div>

{merged.labelPosition === "after" && renderLabel()}
</div>
);
};

export default Switch;
3 changes: 3 additions & 0 deletions packages/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { JSX } from "solid-js";

export type HTMLInputElementProps = JSX.HTMLElementTags["input"];
export type HTMLDivElementProps = JSX.HTMLElementTags["div"];

export type FluentMouseEvent<T extends HTMLElement> = MouseEvent & {
currentTarget: T;
};
Expand Down
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const examples = [
lazy(() => import("./components/spinner")),
lazy(() => import("./components/progress")),
lazy(() => import("./components/input")),
lazy(() => import("./components/switch")),
];

//const menus = ["Button"];
Expand Down
62 changes: 62 additions & 0 deletions src/components/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createSignal } from "solid-js";
import Switch from "~/components/switch";

const Checked = () => {
const [checked, setChecked] = createSignal(true);
const onChange = (ev: boolean) => {
setChecked(ev);
console.log(ev);
};

return (
<Switch
checked={checked()}
onChange={onChange}
label={checked() ? "Checked" : "Unchecked"}
/>
);
};

const SwitchDemo = () => {
return (
<div>
<div>
<Switch />
<Switch label="This is a switch" />
</div>

<div>
<Checked />
</div>

<div>
<Switch disabled label="Unchecked and disabled" />
</div>

<div>
<Switch disabled label="Checked and disabled" checked />
</div>

<div>
<Switch label="With label before" labelPosition="before" />

<Switch label="With label above" labelPosition="above" />

<Switch label="With label after" labelPosition="after" />
</div>

<div>
<Switch
style={{ "max-width": "400px" }}
label="Here is an example of a Switch with a long label and it starts to wrap to a second line."
/>
</div>

<div>
<Switch required label="Required" />
</div>
</div>
);
};

export default SwitchDemo;

0 comments on commit 68b3a0b

Please sign in to comment.