1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Native switch component (#4610)

* Switch component initial draft

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add onClick event

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* onClick fine-tunings

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fine-tuning styles

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix light theme thumb color

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Using native switch in places

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Removing material ui switcher

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Revert "Removing material ui switcher"

This reverts commit 6b9e0a090c.

* Mark Switcher and FormSwitch as deprecated

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Cleaning up

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Using theme-light mixin

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix fetching values from onChange callback

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add custon onChange event with checked prop

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Check for onChange() availability

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix show minimap label

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-12-30 13:58:28 +03:00 committed by GitHub
parent d8dbe51e7a
commit 5bdfea6e31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 262 additions and 53 deletions

View File

@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { Input } from "../input"; import { Input } from "../input";
import { isWindows } from "../../../common/vars"; import { isWindows } from "../../../common/vars";
import { FormSwitch, Switcher } from "../switch"; import { Switch } from "../switch";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers"; import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
import { action } from "mobx"; import { action } from "mobx";
@ -86,16 +86,12 @@ export const Application = observer(() => {
<section id="terminalSelection"> <section id="terminalSelection">
<SubTitle title="Terminal copy & paste" /> <SubTitle title="Terminal copy & paste" />
<FormSwitch <Switch
label="Copy on select and paste on right-click" checked={userStore.terminalCopyOnSelect}
control={ onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
<Switcher >
checked={userStore.terminalCopyOnSelect} Copy on select and paste on right-click
onChange={v => userStore.terminalCopyOnSelect = v.target.checked} </Switch>
name="terminalCopyOnSelect"
/>
}
/>
</section> </section>
<hr/> <hr/>
@ -135,16 +131,9 @@ export const Application = observer(() => {
<section id="other"> <section id="other">
<SubTitle title="Start-up"/> <SubTitle title="Start-up"/>
<FormSwitch <Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
control={ Automatically start Lens on login
<Switcher </Switch>
checked={userStore.openAtLogin}
onChange={v => userStore.openAtLogin = v.target.checked}
name="startup"
/>
}
label="Automatically start Lens on login"
/>
</section> </section>
<hr /> <hr />

View File

@ -21,7 +21,7 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { FormSwitch, Switcher } from "../switch"; import { Switch } from "../switch";
import { Select } from "../select"; import { Select } from "../select";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { SubHeader } from "../layout/sub-header"; import { SubHeader } from "../layout/sub-header";
@ -45,15 +45,12 @@ export const Editor = observer(() => {
<section> <section>
<div className="flex gaps justify-space-between"> <div className="flex gaps justify-space-between">
<div className="flex gaps align-center"> <div className="flex gaps align-center">
<FormSwitch <Switch
label={<SubHeader compact>Show minimap</SubHeader>} checked={editorConfiguration.minimap.enabled}
control={ onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
<Switcher >
checked={editorConfiguration.minimap.enabled} Show minimap
onChange={(evt, checked) => editorConfiguration.minimap.enabled = checked} </Switch>
/>
}
/>
</div> </div>
<div className="flex gaps align-center"> <div className="flex gaps align-center">
<SubHeader compact>Position</SubHeader> <SubHeader compact>Position</SubHeader>

View File

@ -26,7 +26,7 @@ import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-s
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { bundledKubectlPath } from "../../../main/kubectl"; import { bundledKubectlPath } from "../../../main/kubectl";
import { SelectOption, Select } from "../select"; import { SelectOption, Select } from "../select";
import { FormSwitch, Switcher } from "../switch"; import { Switch } from "../switch";
import { packageMirrors } from "../../../common/user-store/preferences-helpers"; import { packageMirrors } from "../../../common/user-store/preferences-helpers";
export const KubectlBinaries = observer(() => { export const KubectlBinaries = observer(() => {
@ -48,16 +48,12 @@ export const KubectlBinaries = observer(() => {
<> <>
<section> <section>
<SubTitle title="Kubectl binary download"/> <SubTitle title="Kubectl binary download"/>
<FormSwitch <Switch
control={ checked={userStore.downloadKubectlBinaries}
<Switcher onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
checked={userStore.downloadKubectlBinaries} >
onChange={v => userStore.downloadKubectlBinaries = v.target.checked} Download kubectl binaries matching the Kubernetes cluster version
name="kubectl-download" </Switch>
/>
}
label="Download kubectl binaries matching the Kubernetes cluster version"
/>
</section> </section>
<section> <section>

View File

@ -24,10 +24,11 @@ import React from "react";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { Input } from "../input"; import { Input } from "../input";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { FormSwitch, Switcher } from "../switch"; import { Switch } from "../switch";
export const LensProxy = observer(() => { export const LensProxy = observer(() => {
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || ""); const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
const store = UserStore.getInstance();
return ( return (
<section id="proxy"> <section id="proxy">
@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
<section className="small"> <section className="small">
<SubTitle title="Certificate Trust"/> <SubTitle title="Certificate Trust"/>
<FormSwitch <Switch checked={store.allowUntrustedCAs} onChange={() => store.allowUntrustedCAs = !store.allowUntrustedCAs}>
control={ Allow untrusted Certificate Authorities
<Switcher </Switch>
checked={UserStore.getInstance().allowUntrustedCAs}
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
name="startup"
/>
}
label="Allow untrusted Certificate Authorities"
/>
<small className="hint"> <small className="hint">
This will make Lens to trust ANY certificate authority without any validations.{" "} This will make Lens to trust ANY certificate authority without any validations.{" "}
Needed with some corporate proxies that do certificate re-writing.{" "} Needed with some corporate proxies that do certificate re-writing.{" "}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { fireEvent, render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { Switch } from "..";
describe("<Switch/>", () => {
it("renders w/o errors", () => {
const { container } = render(<Switch />);
expect(container).toBeInstanceOf(HTMLElement);
});
it("render label text", () => {
const { getByLabelText } = render(<Switch>Test label</Switch>);
expect(getByLabelText("Test label")).toBeTruthy();
});
it("passes disabled and checked attributes to input", () => {
const { container } = render(<Switch checked disabled/>);
const checkbox = container.querySelector("input[type=checkbox]");
expect(checkbox).toHaveAttribute("disabled");
expect(checkbox).toHaveAttribute("checked");
});
it("onClick event fired", () => {
const onClick = jest.fn();
const { getByTestId } = render(<Switch onClick={onClick}/>);
const switcher = getByTestId("switch");
fireEvent.click(switcher);
expect(onClick).toHaveBeenCalled();
});
it("onClick event not fired for disabled item", () => {
const onClick = jest.fn();
const { getByTestId } = render(<Switch onClick={onClick} disabled/>);
const switcher = getByTestId("switch");
fireEvent.click(switcher);
expect(onClick).not.toHaveBeenCalled();
});
});

View File

@ -35,6 +35,9 @@ const useStyles = makeStyles({
}, },
}); });
/**
* @deprecated Use <Switch/> instead from "../switch.tsx".
*/
export function FormSwitch(props: FormControlLabelProps) { export function FormSwitch(props: FormControlLabelProps) {
const classes = useStyles(); const classes = useStyles();

View File

@ -21,3 +21,4 @@
export * from "./switcher"; export * from "./switcher";
export * from "./form-switcher"; export * from "./form-switcher";
export * from "./switch";

View File

@ -0,0 +1,121 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.Switch {
--thumb-size: 2rem;
--thumb-color: hsl(0 0% 100%);
--thumb-color-highlight: hsl(0 0% 100% / 25%);
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-color-inactive: hsl(80 0% 35%);
--track-color-active: hsl(110, 60%, 60%);
--thumb-position: 0%;
--thumb-transition-duration: .25s;
--hover-highlight-size: 0;
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
cursor: pointer;
user-select: none;
color: var(--textColorAccent);
font-weight: 500;
& > input {
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
appearance: none;
pointer-events: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
transition: background-color .25s ease;
&::before {
content: "";
cursor: pointer;
pointer-events: auto;
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
background: var(--thumb-color);
box-shadow: 0 0 0 var(--hover-highlight-size) var(--thumb-color-highlight);
border-radius: 50%;
transform: translateX(var(--thumb-position));
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}
&:not(:disabled):hover::before {
--hover-highlight-size: .5rem;
}
&:checked {
background: var(--track-color-active);
--thumb-position: 100%;
}
&:disabled {
--track-color-inactive: hsl(80 0% 30%);
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 40%);
}
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--blue);
}
}
&.disabled {
cursor: not-allowed;
}
}
@include theme-light {
.Switch {
--thumb-color-highlight: hsl(0 0% 0% / 25%);
& > input {
&:disabled {
--track-color-inactive: hsl(80 0% 80%);
}
}
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./switch.module.scss";
import React, { ChangeEvent, HTMLProps } from "react";
import { cssNames } from "../../utils";
interface Props extends Omit<HTMLProps<HTMLInputElement>, "onChange"> {
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
}
export function Switch({ children, disabled, onChange, ...props }: Props) {
return (
<label className={cssNames(styles.Switch, { [styles.disabled]: disabled })} data-testid="switch">
{children}
<input type="checkbox" role="switch" disabled={disabled} onChange={(event) => onChange?.(props.checked, event)} {...props}/>
</label>
);
}

View File

@ -31,6 +31,9 @@ interface Props extends SwitchProps {
classes: Styles; classes: Styles;
} }
/**
* @deprecated Use <Switch/> instead from "../switch.tsx".
*/
export const Switcher = withStyles((theme: Theme) => export const Switcher = withStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {