feat(lite/component): Radio, Checkbox, Select, Input, Toggle (#6426)
This commit is contained in:
parent
b566e0fd46
commit
72a3a9f04f
190
@xen-orchestra/lite/src/components/form/FormCheckbox.vue
Normal file
190
@xen-orchestra/lite/src/components/form/FormCheckbox.vue
Normal 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>
|
275
@xen-orchestra/lite/src/components/form/FormInput.vue
Normal file
275
@xen-orchestra/lite/src/components/form/FormInput.vue
Normal 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>
|
33
@xen-orchestra/lite/src/components/form/FormLabel.vue
Normal file
33
@xen-orchestra/lite/src/components/form/FormLabel.vue
Normal 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>
|
10
@xen-orchestra/lite/src/components/form/FormRadio.vue
Normal file
10
@xen-orchestra/lite/src/components/form/FormRadio.vue
Normal 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>
|
14
@xen-orchestra/lite/src/components/form/FormSelect.vue
Normal file
14
@xen-orchestra/lite/src/components/form/FormSelect.vue
Normal 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>
|
10
@xen-orchestra/lite/src/components/form/FormToggle.vue
Normal file
10
@xen-orchestra/lite/src/components/form/FormToggle.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user