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