feat(lite/component): Radio, Checkbox, Select, Input, Toggle (#6426)

This commit is contained in:
Thierry Goettelmann 2022-10-05 11:01:14 +02:00 committed by Julien Fontanet
parent b566e0fd46
commit 72a3a9f04f
6 changed files with 532 additions and 0 deletions

View File

@ -0,0 +1,190 @@
<template>
<component
:is="hasLabel ? 'span' : 'label'"
:class="`form-${type}`"
v-bind="wrapperAttrs"
>
<input
v-model="value"
:disabled="isLabelDisabled || disabled"
:type="type === 'radio' ? 'radio' : 'checkbox'"
class="input"
v-bind="$attrs"
/>
<span class="fake-checkbox">
<UiIcon :fixed-width="false" :icon="icon" class="icon" />
</span>
</component>
</template>
<script lang="ts">
export default {
name: "FormCheckbox",
inheritAttrs: false,
};
</script>
<script lang="ts" setup>
import {
type HTMLAttributes,
type InputHTMLAttributes,
computed,
inject,
ref,
} from "vue";
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/UiIcon.vue";
// Temporary workaround for https://github.com/vuejs/core/issues/4294
interface Props extends Omit<InputHTMLAttributes, ""> {
modelValue?: unknown;
disabled?: boolean;
wrapperAttrs?: HTMLAttributes;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(event: "update:modelValue", value: boolean): void;
}>();
const value = useVModel(props, "modelValue", emit);
const type = inject<"checkbox" | "radio" | "toggle">("inputType", "checkbox");
const hasLabel = inject("hasLabel", false);
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
</script>
<style lang="postcss" scoped>
.form-toggle,
.form-checkbox,
.form-radio {
display: inline-flex;
height: 1.25em;
--checkbox-border-width: 0.0625em;
}
.form-radio {
--checkbox-border-radius: 0.625em;
--checkbox-icon-size: 0.625em;
}
.form-checkbox {
--checkbox-border-radius: 0.25em;
--checkbox-icon-size: 1em;
}
.form-checkbox,
.form-radio {
width: 1.25em;
.fake-checkbox {
width: 1.25em;
--background-color: var(--background-color-primary);
}
.icon {
transition: opacity 0.125s ease-in-out;
}
.input + .fake-checkbox > .icon {
opacity: 0;
}
.input:checked + .fake-checkbox > .icon {
opacity: 1;
}
}
.form-toggle {
width: 2.5em;
--checkbox-border-radius: 0.625em;
--checkbox-icon-size: 0.875em;
.fake-checkbox {
width: 2.5em;
--background-color: var(--color-blue-scale-400);
}
.icon {
transform: translateX(-0.7em);
transition: transform 0.125s ease-in-out;
}
.input:checked + .fake-checkbox > .icon {
transform: translateX(0.7em);
}
}
.input {
font-size: inherit;
position: absolute;
pointer-events: none;
opacity: 0;
}
.icon {
font-size: var(--checkbox-icon-size);
position: absolute;
color: var(--color-blue-scale-500);
filter: drop-shadow(0 0.0625em 0.5em rgba(0, 0, 0, 0.1))
drop-shadow(0 0.1875em 0.1875em rgba(0, 0, 0, 0.06))
drop-shadow(0 0.1875em 0.25em rgba(0, 0, 0, 0.08));
}
.fake-checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
height: 1.25em;
border: var(--checkbox-border-width) solid var(--border-color);
border-radius: var(--checkbox-border-radius);
background-color: var(--background-color);
box-shadow: var(--shadow-100);
transition: background-color 0.125s ease-in-out,
border-color 0.125s ease-in-out;
--border-color: var(--color-blue-scale-400);
}
.input:disabled {
& + .fake-checkbox {
cursor: not-allowed;
--background-color: var(--background-color-secondary);
--border-color: var(--color-blue-scale-400);
}
&:checked + .fake-checkbox {
--border-color: transparent;
--background-color: var(--color-extra-blue-l60);
}
}
.input:not(:disabled) {
&:hover + .fake-checkbox,
&:focus + .fake-checkbox {
--border-color: var(--color-extra-blue-l40);
}
&:active + .fake-checkbox {
--border-color: var(--color-extra-blue-l20);
}
&:checked + .fake-checkbox {
--border-color: transparent;
--background-color: var(--color-extra-blue-base);
}
&:checked:hover + .fake-checkbox,
&:checked:focus + .fake-checkbox {
--background-color: var(--color-extra-blue-d20);
}
&:checked:active + .fake-checkbox {
--background-color: var(--color-extra-blue-d40);
}
}
</style>

View File

@ -0,0 +1,275 @@
<template>
<span :class="wrapperClass" v-bind="wrapperAttrs">
<input
v-if="!isSelect"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="input"
v-bind="$attrs"
/>
<template v-else>
<select
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="select"
v-bind="$attrs"
>
<slot />
</select>
<span class="caret">
<UiIcon :fixed-width="false" :icon="faAngleDown" />
</span>
</template>
<span v-if="before !== undefined" class="before">
<template v-if="typeof before === 'string'">{{ before }}</template>
<UiIcon v-else :icon="before" class="before" />
</span>
<span v-if="after !== undefined" class="after">
<template v-if="typeof after === 'string'">{{ after }}</template>
<UiIcon v-else :icon="after" class="after" />
</span>
</span>
</template>
<script lang="ts">
export default {
name: "FormInput",
inheritAttrs: false,
};
</script>
<script lang="ts" setup>
import { isEmpty } from "lodash-es";
import {
type HTMLAttributes,
type InputHTMLAttributes,
computed,
inject,
ref,
} from "vue";
import type { Color } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/UiIcon.vue";
// Temporary workaround for https://github.com/vuejs/core/issues/4294
interface Props extends Omit<InputHTMLAttributes, ""> {
modelValue?: unknown;
color?: Color;
before?: Omit<IconDefinition, ""> | string;
after?: Omit<IconDefinition, ""> | string;
beforeWidth?: string;
afterWidth?: string;
disabled?: boolean;
right?: boolean;
wrapperAttrs?: HTMLAttributes;
}
const props = withDefaults(defineProps<Props>(), { color: "info" });
const emit = defineEmits<{
(event: "update:modelValue", value: any): void;
}>();
const value = useVModel(props, "modelValue", emit);
const empty = computed(() => isEmpty(props.modelValue));
const isSelect = inject("isSelect", false);
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const wrapperClass = computed(() => [
isSelect ? "form-select" : "form-input",
{
disabled: props.disabled || isLabelDisabled.value,
empty: empty.value,
},
]);
const inputClass = computed(() => [
props.color,
{
right: props.right,
"has-before": props.before !== undefined,
"has-after": props.after !== undefined,
},
]);
</script>
<style lang="postcss" scoped>
.form-input,
.form-select {
display: inline-grid;
align-items: stretch;
--before-width: v-bind('beforeWidth ?? "1.75em"');
--after-width: v-bind('afterWidth ?? "1.625em"');
--caret-width: 1.5em;
--text-color: var(--color-blue-scale-100);
&.empty {
--text-color: var(--color-blue-scale-300);
}
&.disabled {
--text-color: var(--color-blue-scale-400);
}
}
.form-input {
grid-template-columns: var(--before-width) auto var(--after-width);
}
.form-select {
grid-template-columns:
var(--before-width)
auto
var(--after-width)
var(--caret-width);
}
.input,
.select {
font-size: 1em;
height: 2em;
margin: 0;
color: var(--text-color);
border: 0.0625em solid var(--border-color);
border-radius: 0.5em;
outline: none;
background-color: var(--background-color);
box-shadow: var(--shadow-100);
grid-row: 1 / 2;
grid-column: 1 / 4;
&.right {
text-align: right;
}
--background-color: var(--background-color-primary);
--border-color: var(--color-blue-scale-400);
&:disabled {
cursor: not-allowed;
--background-color: var(--background-color-secondary);
}
&:not(:disabled) {
&.info {
&:hover {
--border-color: var(--color-extra-blue-l60);
}
&:active {
--border-color: var(--color-extra-blue-l40);
}
&:focus {
--border-color: var(--color-extra-blue-base);
}
}
&.success {
--border-color: var(--color-green-infra-base);
&:hover {
--border-color: var(--color-green-infra-l60);
}
&:active {
--border-color: var(--color-green-infra-l40);
}
&:focus {
--border-color: var(--color-green-infra-base);
}
}
&.warning {
--border-color: var(--color-orange-world-base);
&:hover {
--border-color: var(--color-orange-world-l60);
}
&:active {
--border-color: var(--color-orange-world-l40);
}
&:focus {
--border-color: var(--color-orange-world-base);
}
}
&.error {
--border-color: var(--color-red-vates-base);
&:hover {
--border-color: var(--color-red-vates-l60);
}
&:active {
--border-color: var(--color-red-vates-l40);
}
&:focus-within {
--border-color: var(--color-red-vates-base);
}
}
}
}
.input {
padding: 0 0.625em 0 0.625em;
&.has-before {
padding-left: calc(var(--before-width) + 0.25em);
}
&.has-after {
padding-right: calc(var(--after-width) + 0.25em);
}
}
.select {
min-width: fit-content;
padding: 0 calc(var(--caret-width) + 0.25em) 0 0.625em;
grid-column: 1 / 5;
appearance: none;
&.has-before {
padding-left: calc(var(--before-width) + 0.25em);
}
&.has-after {
padding-right: calc(var(--after-width) + 0.25em + var(--caret-width));
}
}
.before,
.after,
.caret {
display: inline-flex;
align-items: center;
pointer-events: none;
color: var(--text-color);
grid-row: 1 / 2;
}
.before {
justify-self: end;
grid-column: 1 / 2;
}
.after {
justify-self: start;
grid-column: 3 / 4;
}
.caret {
justify-self: start;
grid-column: 4 / 5;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<label :class="{ disabled }" class="form-label">
<slot />
</label>
</template>
<script lang="ts" setup>
import { computed, provide } from "vue";
const props = defineProps<{
disabled?: boolean;
}>();
provide("hasLabel", true);
provide(
"isLabelDisabled",
computed(() => props.disabled)
);
</script>
<style lang="postcss" scoped>
.form-label {
font-size: 1.6rem;
display: inline-flex;
align-items: center;
gap: 0.625em;
&.disabled {
cursor: not-allowed;
color: var(--color-blue-scale-300);
}
}
</style>

View File

@ -0,0 +1,10 @@
<template>
<FormCheckbox />
</template>
<script lang="ts" setup>
import { provide } from "vue";
import FormCheckbox from "@/components/form/FormCheckbox.vue";
provide("inputType", "radio");
</script>

View File

@ -0,0 +1,14 @@
<template>
<FormInput>
<slot />
</FormInput>
</template>
<script lang="ts" setup>
import { provide } from "vue";
import FormInput from "@/components/form/FormInput.vue";
provide("isSelect", true);
</script>
<style lang="postcss" scoped></style>

View File

@ -0,0 +1,10 @@
<template>
<FormCheckbox />
</template>
<script lang="ts" setup>
import { provide } from "vue";
import FormCheckbox from "@/components/form/FormCheckbox.vue";
provide("inputType", "toggle");
</script>