Skip to content

Commit

Permalink
feat(QSwitch): add component (#280)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey authored May 5, 2022
1 parent 61fa5a7 commit 3f01f42
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 2 deletions.
12 changes: 12 additions & 0 deletions src/qComponents/QSwitch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { withInstall } from '../helpers';

import Switch from './src/QSwitch.vue';

export const QSwitch = withInstall(Switch);

export type {
QSwitchPropModelValue,
QSwitchPropActiveValue,
QSwitchPropInactiveValue,
QSwitchProps
} from './src/types';
152 changes: 152 additions & 0 deletions src/qComponents/QSwitch/src/QSwitch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<template>
<label
class="q-switch"
:class="classes"
:tabindex="tabIndex"
:aria-disabled="isDisabled"
@keyup.enter="handleSwitcherChange"
@keyup.space.prevent="handleSwitcherChange"
@click.prevent="handleSwitcherChange"
>
<input
class="q-switch__checkbox"
type="checkbox"
:checked="isChecked"
tabindex="-1"
/>
<div class="q-switch__wrapper">
<div class="q-switch__handle">
<div
v-if="loading"
class="q-icon-reverse"
/>
</div>
</div>
</label>
</template>

<script lang="ts">
import { defineComponent, computed, inject, watch } from 'vue';
import type { QFormItemProvider, QFormProvider } from '@/qComponents';
import type { ClassValue, Nullable } from '#/helpers';
import type {
QSwitchProps,
QSwitchInstance,
QSwitchTabIndexType,
QSwitchEmitValueType
} from './types';
export default defineComponent({
name: 'QSwitch',
componentName: 'QSwitch',
props: {
/**
* default to v-model
*/
modelValue: {
type: [Boolean, String, Number],
default: false
},
/**
* value for active QSwitch state
*/
activeValue: {
type: [Boolean, String, Number],
default: true
},
/**
* value for inactive QSwitch state
*/
inactiveValue: {
type: [Boolean, String, Number],
default: false
},
/**
* whether QSwitch is disabled
*/
disabled: {
type: Boolean,
default: false
},
/**
* whether to show loader inside the QSwitch
*/
loading: {
type: Boolean,
default: false
},
/**
* validate parent form if present
*/
validateEvent: {
type: Boolean,
default: true
}
},
emits: [
/**
* triggers when model updates
*/
'update:modelValue',
/**
* alias for update:modelValue
*/
'change'
],
setup(props: QSwitchProps, ctx): QSwitchInstance {
const qFormItem = inject<Nullable<QFormItemProvider>>('qFormItem', null);
const qForm = inject<Nullable<QFormProvider>>('qForm', null);
const isChecked = computed<boolean>(
() => props.modelValue === props.activeValue
);
const isDisabled = computed<boolean>(
() => props.disabled || (qForm?.disabled.value ?? false)
);
const tabIndex = computed<QSwitchTabIndexType>(() =>
props.disabled ? -1 : 0
);
const classes = computed<ClassValue>(() => ({
'q-switch_active': isChecked.value,
'q-switch_disabled': isDisabled.value,
'q-switch_loading': Boolean(props.loading)
}));
const emitChange = (value: QSwitchEmitValueType): void => {
ctx.emit('update:modelValue', value);
ctx.emit('change', value);
};
const handleSwitcherChange = (): void => {
if (props.disabled || props.loading) return;
const value = isChecked.value ? props.inactiveValue : props.activeValue;
emitChange(value);
};
watch(
() => props.modelValue,
() => {
if (props.validateEvent) qFormItem?.validateField('change');
}
);
return {
isChecked,
tabIndex,
classes,
isDisabled,
handleSwitcherChange
};
}
});
</script>
86 changes: 86 additions & 0 deletions src/qComponents/QSwitch/src/q-switch.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.q-switch {
--switch-main-color: var(--color-primary);
--switch-handle-width: 24px;
--switch-wrapper-translate-x: calc(-100% + var(--switch-handle-width));

position: relative;
display: block;
width: 40px;
overflow: hidden;
cursor: pointer;
background: var(--color-tertiary-gray-ultra-light);
border-radius: 12px;
box-shadow: 4px 4px 8px rgb(174 174 192 / 40%),
1px 1px 3px rgb(174 174 192 / 40%);

&__checkbox {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
margin: 0;
outline: none;
opacity: 0;
}

&__wrapper {
width: 100%;
background-color: var(--switch-main-color);
border-radius: 12px;
transition: transform 0.2s ease-out;
transform: translateX(var(--switch-wrapper-translate-x));
}

&__handle {
display: flex;
align-items: center;
justify-content: center;
width: var(--switch-handle-width);
height: 24px;
margin-left: auto;
background-color: var(--color-tertiary-gray-ultra-light);
border: 2px solid var(--switch-main-color);
border-radius: 12px;
transition: width 0.2s ease-out;

.q-icon-reverse {
font-size: 16px;
color: var(--switch-main-color);
animation: rotating 2s linear infinite;
}
}

&_disabled {
--switch-main-color: var(--color-tertiary-gray-ultra-darker);

cursor: not-allowed;
box-shadow: var(--box-shadow-pressed);
}

&_active {
--switch-wrapper-translate-x: 0;

&:active:not(.q-switch_disabled, .q-switch_loading) {
--switch-handle-width: 28px;
}
}

&_loading {
cursor: progress;
}

&.focus-visible {
outline: none;
}

&:active:not(&_disabled, &_loading, &_active) {
--switch-handle-width: 28px;
}

&:focus-visible:not(&_disabled, &_loading),
&.focus-visible:not(&_disabled, &_loading) {
--switch-main-color: var(--color-primary-darker);
}
}
29 changes: 29 additions & 0 deletions src/qComponents/QSwitch/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ComputedRef } from 'vue';
import type { Nullable, ClassValue } from '#/helpers';

export type QSwitchPropModelValue = Nullable<boolean | string | number>;
export type QSwitchPropActiveValue = QSwitchPropModelValue;
export type QSwitchPropInactiveValue = QSwitchPropActiveValue;

export type QSwitchTabIndexType = -1 | 0;

export type QSwitchEmitValueType =
| QSwitchPropInactiveValue
| QSwitchPropActiveValue;

export interface QSwitchProps {
modelValue: QSwitchPropModelValue;
activeValue: QSwitchPropActiveValue;
inactiveValue: QSwitchPropInactiveValue;
disabled: Nullable<boolean>;
loading: Nullable<boolean>;
validateEvent: Nullable<boolean>;
}

export interface QSwitchInstance {
classes: ComputedRef<ClassValue>;
isChecked: ComputedRef<boolean>;
tabIndex: ComputedRef<number>;
isDisabled: ComputedRef<boolean>;
handleSwitcherChange: () => void;
}
4 changes: 4 additions & 0 deletions src/qComponents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { QRow } from './QRow';
import { QScrollbar } from './QScrollbar';
import { QSelect } from './QSelect';
import { QSlider } from './QSlider';
import { QSwitch } from './QSwitch';
import { QTable } from './QTable';
import { QTabPane } from './QTabPane';
import { QTabs } from './QTabs';
Expand Down Expand Up @@ -76,6 +77,7 @@ import './QRow/src/q-row.scss';
import './QScrollbar/src/q-scrollbar.scss';
import './QSelect/src/q-select.scss';
import './QSlider/src/q-slider.scss';
import './QSwitch/src/q-switch.scss';
import './QTable/src/q-table.scss';
import './QTabPane/src/q-tab-pane.scss';
import './QTabs/src/q-tabs.scss';
Expand Down Expand Up @@ -144,6 +146,7 @@ const install = (app: App, config?: ConfigOptions): void => {
app.use(QScrollbar);
app.use(QSelect);
app.use(QSlider);
app.use(QSwitch);
app.use(QTable);
app.use(QTabPane);
app.use(QTabs);
Expand Down Expand Up @@ -184,6 +187,7 @@ export * from './QRow';
export * from './QScrollbar';
export * from './QSelect';
export * from './QSlider';
export * from './QSwitch';
export * from './QTable';
export * from './QTabPane';
export * from './QTabs';
Expand Down
42 changes: 42 additions & 0 deletions stories/components/QSwitch.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Meta, Story } from '@storybook/vue3';
import { defineComponent, ref } from 'vue';

import { QSwitch } from '@/qComponents';
import type { QSwitchProps } from '@/qComponents';

const storyMetadata: Meta = {
title: 'Components/QSwitch',
component: QSwitch,

argTypes: {
modelValue: { control: { type: 'none' } },
validateEvent: { control: { type: 'none' } },
activeValue: { control: { type: 'text' } },
inactiveValue: { control: { type: 'text' } }
}
};

const QSwitchStory: Story<QSwitchProps> = args =>
defineComponent({
setup() {
const isOn = ref(true);

return {
args,
isOn
};
},

template: `
<q-switch
v-model="isOn"
:loading="args.loading"
:disabled="args.disabled"
:active-value="args.activeValue"
:inactive-value="args.inactiveValue"
/>
`
});

export const Default = QSwitchStory.bind({});
export default storyMetadata;
6 changes: 4 additions & 2 deletions vuepress-docs/docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export default defineUserConfig<DefaultThemeOptions, ViteBundlerOptions>({
'/components/QRadio.md',
'/components/QScrollbar.md',
'/components/QTabs.md',
'/components/QSelect.md'
'/components/QSelect.md',
'/components/QSwitch.md'
]
},
// NavbarGroup
Expand Down Expand Up @@ -72,7 +73,8 @@ export default defineUserConfig<DefaultThemeOptions, ViteBundlerOptions>({
'/components/QRadio.md',
'/components/QScrollbar.md',
'/components/QTabs.md',
'/components/QSelect.md'
'/components/QSelect.md',
'/components/QSwitch.md'
]
}
],
Expand Down
Loading

0 comments on commit 3f01f42

Please sign in to comment.