├── public
└── favicon.ico
├── src
├── assets
│ └── tailwind.css
├── types
│ ├── helpers.ts
│ ├── components
│ │ ├── t-tag.ts
│ │ ├── t-input.ts
│ │ ├── t-textarea.ts
│ │ ├── t-card.ts
│ │ ├── t-alert.ts
│ │ ├── t-button.ts
│ │ ├── t-radio.ts
│ │ ├── t-toggle.ts
│ │ ├── t-checkbox.ts
│ │ ├── t-input-group.ts
│ │ ├── t-select.ts
│ │ ├── t-dropdown.ts
│ │ ├── t-modal.ts
│ │ ├── t-dialog.ts
│ │ └── t-rich-select.ts
│ ├── utils.ts
│ ├── misc.ts
│ ├── index.ts
│ ├── vueRouter.ts
│ └── variantCore.ts
├── development
│ ├── TSubmit.vue
│ ├── About.vue
│ ├── App.vue
│ ├── AppMenu.vue
│ ├── router.ts
│ ├── Attributes.vue
│ ├── Multioptions.vue
│ ├── Alert.vue
│ ├── Home.vue
│ ├── Check.vue
│ ├── Options.vue
│ ├── Modal.vue
│ └── RichSelect.vue
├── shims-vue.d.ts
├── use
│ ├── useInjectsConfiguration.ts
│ ├── useInjectsClassesList.ts
│ ├── useInjectsClassesListClass.ts
│ ├── useVModel.ts
│ ├── useMultioptions.ts
│ ├── useConfigurationWithClassesList.ts
│ ├── useMulipleableVModel.ts
│ ├── useConfiguration.ts
│ ├── useActivableOption.ts
│ └── useSelectableOption.ts
├── __tests
│ ├── components
│ │ ├── misc
│ │ │ ├── Transitionable.spec.ts
│ │ │ └── TextPlaceholder.spec.ts
│ │ ├── TRichSelect
│ │ │ ├── RichSelectClearButton.spec.ts
│ │ │ ├── RichSelectTriggerTags.spec.ts
│ │ │ ├── RichSelectDropdown.spec.ts
│ │ │ ├── RichSelectState.spec.ts
│ │ │ └── RichSelectSearchInput.spec.ts
│ │ ├── icons
│ │ │ └── CustomIcon.spec.ts
│ │ ├── TSelect.integration.spec.ts
│ │ ├── TTag.spec.ts
│ │ ├── TSelect
│ │ │ └── TSelectOption.spec.ts
│ │ ├── TCheckbox.integration.spec.ts
│ │ ├── TInput.integration.spec.ts
│ │ ├── TTextarea.integration.spec.ts
│ │ └── TRadio.integration.spec.ts
│ ├── use
│ │ ├── useVModel.spec.ts
│ │ ├── useInjectsClassesList.spec.ts
│ │ ├── useInjectsConfiguration.spec.ts
│ │ ├── useSetup.ts
│ │ ├── useMulipleableVModel.spec.ts
│ │ ├── useMultioptions.spec.ts
│ │ └── useConfigurationWithClassesList.spec.ts
│ ├── utils
│ │ ├── getVariantProps.spec.ts
│ │ ├── svgToVueComponent.spec.ts
│ │ ├── popper.spec.ts
│ │ ├── emitter.spec.ts
│ │ └── createDialogProgramatically.spec.ts
│ ├── testUtils.ts
│ ├── index.spec.ts
│ └── plugin.spec.ts
├── icons
│ ├── CloseIcon.vue
│ ├── CheckCircleIcon.vue
│ ├── CheckmarkIcon.vue
│ ├── CrossCircleIcon.vue
│ ├── SolidCheckCircleIcon.vue
│ ├── SolidInformationCircleIcon.vue
│ ├── InformationCircleIcon.vue
│ ├── ExclamationIcon.vue
│ ├── SelectorIcon.vue
│ ├── SolidQuestionMarkCircleIcon.vue
│ ├── QuestionMarkCircleIcon.vue
│ ├── SolidExclamationIcon.vue
│ ├── SolidCrossCircleIcon.vue
│ ├── CustomIcon.vue
│ └── LoadingIcon.vue
├── main.ts
├── utils
│ ├── popper.ts
│ ├── svgToVueComponent.ts
│ ├── emitter.ts
│ ├── getVariantProps.ts
│ └── createDialogProgramatically.ts
├── components
│ ├── TRichSelect
│ │ ├── RichSelectClearButton.vue
│ │ ├── RichSelectTriggerTags.vue
│ │ ├── RichSelectState.vue
│ │ ├── RichSelectSearchInput.vue
│ │ ├── RichSelectDropdown.vue
│ │ ├── RichSelectTriggerTagsTag.vue
│ │ ├── RichSelectOptionsList.vue
│ │ └── RichSelectTrigger.vue
│ ├── misc
│ │ ├── TextPlaceholder.vue
│ │ └── Transitionable.vue
│ ├── TTag.vue
│ ├── TRadio.vue
│ ├── TCheckbox.vue
│ ├── TSelect
│ │ └── TSelectOption.vue
│ ├── TInput.vue
│ ├── TTextarea.vue
│ ├── TCard.vue
│ ├── TSelect.vue
│ ├── TButton.vue
│ ├── TInputGroup.vue
│ └── TAlert.vue
├── index.ts
└── plugin.ts
├── .gitignore
├── postcss.config.js
├── .github
├── FUNDING.yml
└── workflows
│ └── yarn.yml
├── vite.demo.config.ts
├── tailwind.config.js
├── index.html
├── jest.config.js
├── tsconfig.json
├── vite.config.ts
├── .eslintrc.json
└── package.json
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/variantjs/vue/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | coverage
6 | *.local
7 | yarn-error.log
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 | github: variantjs
3 | custom: https://www.buymeacoffee.com/alfonsobries
4 |
--------------------------------------------------------------------------------
/vite.demo.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [vue()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/types/helpers.ts:
--------------------------------------------------------------------------------
1 | type ObjectWithProperties
= Record;
2 |
3 | type KeysOfType = { [P in keyof T]: T[P] extends TProp? P : never }[keyof T];
4 |
5 | export { ObjectWithProperties, KeysOfType };
6 |
--------------------------------------------------------------------------------
/src/types/components/t-tag.ts:
--------------------------------------------------------------------------------
1 | import { Data, WithVariantProps } from '@variantjs/core';
2 | import { HTMLAttributes } from 'vue';
3 |
4 | export type TTagOptions = WithVariantProps<{
5 | tagName?: string
6 | text?: string
7 | } & HTMLAttributes & Data>;
8 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './src/development/**/*.{html,js,vue,ts}',
4 | './node_modules/@variantjs/core/src/config/**/*.ts'
5 | ],
6 | plugins: [
7 | require('@tailwindcss/forms'),
8 | ],
9 | }
10 |
--------------------------------------------------------------------------------
/.github/workflows/yarn.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: push
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - name: Install modules
9 | run: yarn
10 | - name: Run tests
11 | run: yarn test --coverage
--------------------------------------------------------------------------------
/src/development/TSubmit.vue:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-types */
2 | declare module '*.vue' {
3 | import { DefineComponent } from 'vue';
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
6 | const component: DefineComponent<{}, {}, any>;
7 | export default component;
8 | }
9 |
--------------------------------------------------------------------------------
/src/use/useInjectsConfiguration.ts:
--------------------------------------------------------------------------------
1 | import { Data, WithVariantPropsAndClassesList } from '@variantjs/core';
2 | import { inject } from 'vue';
3 |
4 | export default function useInjectsConfiguration>(): P {
5 | return inject
('configuration', {} as P);
6 | }
7 |
--------------------------------------------------------------------------------
/src/__tests/components/misc/Transitionable.spec.ts:
--------------------------------------------------------------------------------
1 | import Transitionable from '../../../components/misc/Transitionable.vue';
2 |
3 | describe('Transitionable', () => {
4 | it('defaults the classes list to an empty object', () => {
5 | expect(Transitionable.props.classesList.default()).toEqual({});
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/types/components/t-input.ts:
--------------------------------------------------------------------------------
1 | import { Data, WithVariantProps } from '@variantjs/core';
2 | import { InputHTMLAttributes } from 'vue';
3 |
4 | export type TInputValue = string | number | string[] | undefined;
5 |
6 | export type TInputOptions = WithVariantProps<{
7 | modelValue?: TInputValue,
8 | } & InputHTMLAttributes & Data>;
9 |
--------------------------------------------------------------------------------
/src/icons/CloseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/src/types/components/t-textarea.ts:
--------------------------------------------------------------------------------
1 | import { Data, WithVariantProps } from '@variantjs/core';
2 | import { InputHTMLAttributes } from 'vue';
3 |
4 | export type TTextareaValue = string | number | string[] | undefined;
5 |
6 | export type TTextareaOptions = WithVariantProps<{
7 | modelValue?: TTextareaValue,
8 | } & InputHTMLAttributes & Data>;
9 |
--------------------------------------------------------------------------------
/src/types/components/t-card.ts:
--------------------------------------------------------------------------------
1 | import { WithVariantPropsAndClassesList, TCardClassesValidKeys, Data } from '@variantjs/core';
2 | import { HTMLAttributes } from 'vue';
3 |
4 | export type TCardOptions = WithVariantPropsAndClassesList<{
5 | tagName?: string
6 | body?: string
7 | header?: string
8 | footer?: string
9 | } & HTMLAttributes & Data, TCardClassesValidKeys>;
10 |
--------------------------------------------------------------------------------
/src/icons/CheckCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/CheckmarkIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/CrossCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/icons/SolidCheckCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/SolidInformationCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/__tests/components/TRichSelect/RichSelectClearButton.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import RichSelectClearButton from '../../../components/TRichSelect/RichSelectClearButton.vue';
3 |
4 | describe('RichSelectClearButton', () => {
5 | it('renders the component', () => {
6 | const wrapper = shallowMount(RichSelectClearButton);
7 | expect(wrapper.vm.$el.tagName).toBe('BUTTON');
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/icons/InformationCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/use/useInjectsClassesList.ts:
--------------------------------------------------------------------------------
1 | import { CSSClassesList } from '@variantjs/core';
2 | import { ComputedRef, computed } from 'vue';
3 | import useInjectsConfiguration from './useInjectsConfiguration';
4 |
5 | export default function useInjectsClassesList(): ComputedRef {
6 | const configuration = useInjectsConfiguration();
7 |
8 | return computed((): CSSClassesList => configuration.classesList || {});
9 | }
10 |
--------------------------------------------------------------------------------
/src/use/useInjectsClassesListClass.ts:
--------------------------------------------------------------------------------
1 | import { CSSClass, get } from '@variantjs/core';
2 | import { ComputedRef, computed } from 'vue';
3 | import useInjectsConfiguration from './useInjectsConfiguration';
4 |
5 | export default function useInjectsClassesListClass(property: string): ComputedRef {
6 | const configuration = useInjectsConfiguration();
7 |
8 | return computed((): CSSClass => get(configuration.classesList, property, ''));
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/ExclamationIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/icons/SelectorIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/icons/SolidQuestionMarkCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/QuestionMarkCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/icons/SolidExclamationIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/icons/SolidCrossCircleIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'jsdom',
3 | preset: '@vue/cli-plugin-unit-jest/presets/typescript',
4 | collectCoverageFrom: [
5 | "src/**/*.{vue,ts}",
6 | "!**/node_modules/**",
7 | "!**/*.d.ts"
8 | ],
9 | coverageReporters: ["text", "json", "html"],
10 | testMatch: [ "**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)" ],
11 | coveragePathIgnorePatterns: [
12 | "/node_modules/",
13 | "/src/main.ts",
14 | "/src/App.vue",
15 | "/src/development/",
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/types/components/t-alert.ts:
--------------------------------------------------------------------------------
1 | import { WithVariantPropsAndClassesList, TAlertClassesValidKeys, Data } from '@variantjs/core';
2 | import { HTMLAttributes } from 'vue';
3 | import { IconProp } from '../misc';
4 |
5 | export type TAlertOptions = WithVariantPropsAndClassesList<{
6 | text?: string,
7 | tagName?: string,
8 | bodyTagName?: string,
9 | dismissible?: boolean,
10 | show?: boolean,
11 | timeout?: number,
12 | animate?: boolean,
13 | closeIcon?: IconProp,
14 | } & HTMLAttributes & Data, TAlertClassesValidKeys>;
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "declaration": true,
6 | "outDir": "./dist",
7 | "moduleResolution": "node",
8 | "strict": true,
9 | "jsx": "preserve",
10 | "sourceMap": true,
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true,
13 | "lib": ["esnext", "dom"],
14 | "types": ["vite/client", "jest"],
15 | },
16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/src/use/useVModel.ts:
--------------------------------------------------------------------------------
1 | import { Data } from '@variantjs/core';
2 | import {
3 | getCurrentInstance, Ref, watch, ref,
4 | } from 'vue';
5 |
6 | export default function useVModel(
7 | props: P,
8 | key: K,
9 | ): Ref
{
10 | const vm = getCurrentInstance();
11 |
12 | const localValue = ref(props[key]) as Ref
;
13 |
14 | watch(localValue, (value) => {
15 | vm?.emit(`update:${key}`, value);
16 | });
17 |
18 | watch(() => props[key], (value) => {
19 | localValue.value = value;
20 | });
21 |
22 | return localValue;
23 | }
24 |
--------------------------------------------------------------------------------
/src/development/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | VariantJS
5 |
6 |
7 | Development playground used for testing purposes.
8 |
9 |
10 | Made by love by @alfonsobries
11 |
12 |
13 |
14 |
15 |
26 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import './assets/tailwind.css';
2 | import { createApp } from 'vue';
3 | import plugin from './plugin';
4 | import { VariantJSConfiguration } from './types/variantCore';
5 |
6 | import App from './development/App.vue';
7 |
8 | import router from './development/router';
9 |
10 | const app = createApp(App);
11 |
12 | app.use(router);
13 |
14 | const configuration: VariantJSConfiguration = {
15 | TCard: {
16 | classes: {
17 | wrapper: 'bg-white border border-gray-100 rounded shadow-sm w-full',
18 | },
19 | },
20 | };
21 |
22 | app.use(plugin, configuration);
23 |
24 | app.mount('#app');
25 |
--------------------------------------------------------------------------------
/src/types/components/t-button.ts:
--------------------------------------------------------------------------------
1 | import { Data, WithVariantProps } from '@variantjs/core';
2 | import { ButtonHTMLAttributes } from 'vue';
3 | import { VueRouteAriaCurrentValue, VueRouteRouteLocationRaw } from '../vueRouter';
4 |
5 | type RouterLinkProps = {
6 | to?: VueRouteRouteLocationRaw,
7 | replace?: boolean,
8 | activeClass?: string,
9 | exactActiveClass?: string,
10 | custom?: boolean,
11 | ariaCurrentValue?: VueRouteAriaCurrentValue,
12 | };
13 |
14 | export type TButtonOptions = WithVariantProps<{
15 | tagName?: string
16 | href?: string
17 | } & RouterLinkProps & ButtonHTMLAttributes & Data>;
18 |
--------------------------------------------------------------------------------
/src/types/components/t-radio.ts:
--------------------------------------------------------------------------------
1 | import { Data, WithVariantProps } from '@variantjs/core';
2 | import { InputHTMLAttributes } from 'vue';
3 | import { ObjectWithProperties } from '../helpers';
4 |
5 | // eslint-disable-next-line @typescript-eslint/ban-types
6 | type TRadioSimpleValue = string | number | boolean | undefined | null | Date | Function | symbol;
7 | export type TRadioValue = TRadioSimpleValue | TRadioSimpleValue[] | ObjectWithProperties;
8 |
9 | export type TRadioOptions = WithVariantProps<{
10 | modelValue?: TRadioValue
11 | } & InputHTMLAttributes & {
12 | type?: 'radio'
13 | } & Data>;
14 |
--------------------------------------------------------------------------------
/src/types/components/t-toggle.ts:
--------------------------------------------------------------------------------
1 | import { WithVariantPropsAndClassesList, TToggleClassesValidKeys, Data } from '@variantjs/core';
2 | import { HTMLAttributes } from 'vue';
3 | import { TCheckboxValue } from './t-checkbox';
4 |
5 | export type TToggleValue = TCheckboxValue;
6 |
7 | export type TToggleOptions = WithVariantPropsAndClassesList<{
8 | name?: string,
9 | modelValue?: TToggleValue,
10 | value?: TToggleValue,
11 | uncheckedValue?: TToggleValue,
12 | checked?: boolean,
13 | disabled?: boolean,
14 | checkedPlaceholder?: string,
15 | uncheckedPlaceholder?: string,
16 | } & HTMLAttributes & Data, TToggleClassesValidKeys>;
17 |
--------------------------------------------------------------------------------
/src/types/components/t-checkbox.ts:
--------------------------------------------------------------------------------
1 | import { Data, WithVariantProps } from '@variantjs/core';
2 | import { InputHTMLAttributes } from 'vue';
3 | import { ObjectWithProperties } from '../helpers';
4 |
5 | // eslint-disable-next-line @typescript-eslint/ban-types
6 | type TCheckboxSimpleValue = string | number | boolean | undefined | null | Date | Function | symbol;
7 | export type TCheckboxValue = TCheckboxSimpleValue | TCheckboxSimpleValue[] | ObjectWithProperties;
8 |
9 | export type TCheckboxOptions = WithVariantProps<{
10 | modelValue?: TCheckboxValue
11 | } & InputHTMLAttributes & {
12 | type?: 'checkbox'
13 | } & Data>;
14 |
--------------------------------------------------------------------------------
/src/types/utils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | type EmitterFunction = (...args : any[]) => void;
3 |
4 | type EmitterEvents = {
5 | [key: string]: EmitterFunction[]
6 | };
7 |
8 | interface EmitterInterface {
9 | on(name: keyof EmitterEvents, callback: EmitterFunction): void;
10 | once(name: keyof EmitterEvents, callback: EmitterFunction): void;
11 | emit(name: keyof EmitterEvents, ...args: any[]): void;
12 | emit(name: keyof EmitterEvents, ...args: any[]): void;
13 | off(name: keyof EmitterEvents, callback: EmitterFunction): void;
14 | }
15 |
16 | export { EmitterEvents, EmitterFunction, EmitterInterface };
17 |
--------------------------------------------------------------------------------
/src/types/components/t-input-group.ts:
--------------------------------------------------------------------------------
1 | import { WithVariantPropsAndClassesList, TInputGroupClassesValidKeys, Data } from '@variantjs/core';
2 | import { HTMLAttributes } from 'vue';
3 |
4 | export type TInputGroupValidChilElementsKeys = ('label' | 'default' | 'feedback' | 'description')[];
5 |
6 | export type TInputGroupOptions = WithVariantPropsAndClassesList<{
7 | label?: string
8 | description?: string
9 | feedback?: string
10 | body?: string
11 | sortedElements?: TInputGroupValidChilElementsKeys,
12 | tagName?: string,
13 | bodyTagName?: string,
14 | labelTagName?: string,
15 | feedbackTagName?: string,
16 | descriptionTagName?: string,
17 | } & HTMLAttributes & Data, TInputGroupClassesValidKeys>;
18 |
--------------------------------------------------------------------------------
/src/__tests/use/useVModel.spec.ts:
--------------------------------------------------------------------------------
1 | import useVModel from '../../use/useVModel';
2 | import { useSetup } from './useSetup';
3 |
4 | describe('useVModel', () => {
5 | const defaultValue = 'default';
6 |
7 | it('should work with default value', () => {
8 | useSetup(() => {
9 | const data = useVModel({
10 | modelValue: defaultValue,
11 | }, 'modelValue');
12 | expect(data.value).toBe(defaultValue);
13 | });
14 | });
15 |
16 | it('should work with a different value from the default', () => {
17 | useSetup(() => {
18 | const data = useVModel({
19 | otherValue: defaultValue,
20 | }, 'otherValue');
21 | expect(data.value).toBe(defaultValue);
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/types/components/t-select.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Data, InputOptions, NormalizedOption, NormalizedOptions, WithVariantProps,
3 | } from '@variantjs/core';
4 | import { SelectHTMLAttributes } from 'vue';
5 | import { Truthy } from '../misc';
6 |
7 | // eslint-disable-next-line @typescript-eslint/ban-types
8 | export type TSelectValue = string | number | boolean | undefined | null | Date | Function | symbol | TSelectValue[];
9 |
10 | export type TSelectOptions = WithVariantProps<{
11 | modelValue?: TSelectValue,
12 | options?: InputOptions | NormalizedOption[] | NormalizedOptions,
13 | multiple?: Truthy,
14 | normalizeOptions?: boolean,
15 | valueAttribute?: string
16 | textAttribute?: string
17 | } & SelectHTMLAttributes & Data>;
18 |
--------------------------------------------------------------------------------
/src/icons/CustomIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
29 |
--------------------------------------------------------------------------------
/src/utils/popper.ts:
--------------------------------------------------------------------------------
1 | import { Modifier, ModifierArguments } from '@popperjs/core';
2 | import { Data } from '@variantjs/core';
3 |
4 | const sameWidthModifier: Modifier<'sameWidth', Data> = {
5 | name: 'sameWidth',
6 | enabled: true,
7 | phase: 'beforeWrite',
8 | requires: ['computeStyles'],
9 | fn: (options: ModifierArguments): void => {
10 | const { state } = options;
11 | state.styles.popper.width = `${state.rects.reference.width}px`;
12 | },
13 | effect: (options: ModifierArguments): void => {
14 | const { state } = options;
15 | const reference = state.elements.reference as HTMLElement;
16 | state.elements.popper.style.width = `${reference.offsetWidth}px`;
17 | },
18 | };
19 |
20 | export { sameWidthModifier };
21 |
--------------------------------------------------------------------------------
/src/__tests/utils/getVariantProps.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable vue/one-component-per-file */
2 |
3 | import { getVariantProps } from '../../utils/getVariantProps';
4 |
5 | describe('getVariantProps()', () => {
6 | it('get the default variant props', () => {
7 | const props = getVariantProps();
8 | expect(props).toEqual({
9 | classes: {
10 | type: [String, Array, Object],
11 | default: undefined,
12 | },
13 | fixedClasses: {
14 | type: [String, Array, Object],
15 | default: undefined,
16 | },
17 | variants: {
18 | type: Object,
19 | default: undefined,
20 | },
21 | variant: {
22 | type: String,
23 | default: undefined,
24 | },
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/types/components/t-dropdown.ts:
--------------------------------------------------------------------------------
1 | import { Placement, Options } from '@popperjs/core';
2 | import { WithVariantPropsAndClassesList, TDropdownClassesValidKeys, Data } from '@variantjs/core';
3 | import { HTMLAttributes } from 'vue';
4 |
5 | export type TDropdownOptions = WithVariantPropsAndClassesList<{
6 | text?: string,
7 | disabled?: boolean,
8 |
9 | tagName?: string,
10 | dropdownTagName?: string,
11 | dropdownAttributes?: Data,
12 |
13 | toggleOnFocus?: boolean,
14 | toggleOnClick?: boolean,
15 | toggleOnHover?: boolean,
16 |
17 | show?: boolean,
18 |
19 | hideOnLeaveTimeout?: number,
20 |
21 | teleport?: boolean,
22 | teleportTo?: string | HTMLElement,
23 |
24 | placement?: Placement,
25 | popperOptions?: Options,
26 | } & HTMLAttributes & Data, TDropdownClassesValidKeys>;
27 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { defineConfig } from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 | import typescript from '@rollup/plugin-typescript'
5 |
6 | export default defineConfig({
7 | plugins: [vue()],
8 | build: {
9 | minify: false,
10 | sourcemap: true,
11 | lib: {
12 | entry: path.resolve(__dirname, 'src/index.ts'),
13 | name: 'VariantJS',
14 | fileName: (format) => `index.${format}.js`
15 | },
16 | rollupOptions: {
17 | plugins: [
18 | typescript({
19 | "exclude": ["node_modules", 'src/__tests/**/*']
20 | }),
21 | ],
22 | external: ['vue', '@popperjs/core', '@variantjs/core'],
23 | output: {
24 | globals: {
25 | vue: 'Vue'
26 | },
27 | }
28 | }
29 | }
30 | })
31 |
--------------------------------------------------------------------------------
/src/types/components/t-modal.ts:
--------------------------------------------------------------------------------
1 | import { WithVariantPropsAndClassesList, Data, TModalClassesValidKeys } from '@variantjs/core';
2 | import { BodyScrollOptions } from 'body-scroll-lock';
3 | import { HTMLAttributes } from 'vue';
4 |
5 | export type TModalOptions = WithVariantPropsAndClassesList<{
6 | name?: string,
7 | modelValue?: boolean,
8 | modalAttributes?: HTMLAttributes & Data,
9 | tagName?: string
10 | body?: string
11 | header?: string
12 | footer?: string
13 | clickToClose?: boolean,
14 | escToClose?: boolean,
15 | focusOnOpen?: boolean,
16 | disableBodyScroll?: boolean,
17 | bodyScrollLockOptions?: BodyScrollOptions,
18 | teleport?: boolean,
19 | teleportTo?: string | HTMLElement,
20 | noBody?: boolean,
21 | hideCloseButton?: boolean,
22 | } & HTMLAttributes & Data, TModalClassesValidKeys>;
23 |
--------------------------------------------------------------------------------
/src/types/misc.ts:
--------------------------------------------------------------------------------
1 | import { Data, InputOptions } from '@variantjs/core';
2 |
3 | type Truthy = boolean | string;
4 |
5 | // eslint-disable-next-line @typescript-eslint/ban-types
6 | type IconProp = Element | string | (Data & { render?: Function });
7 |
8 | type FetchedOptions = Promise<{
9 | results: InputOptions;
10 | hasMorePages?: boolean;
11 | }>;
12 |
13 | type FetchOptionsFn = (query?: string, nextPage?: number) => FetchedOptions;
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | type PreFetchOptionsFn = (currentValue?: any) => Promise;
17 |
18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
19 | type PromiseRejectFn = ((reason?: any) => void);
20 |
21 | export {
22 | Truthy, IconProp, FetchOptionsFn, FetchedOptions, PromiseRejectFn, PreFetchOptionsFn,
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectClearButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
33 |
--------------------------------------------------------------------------------
/src/components/misc/TextPlaceholder.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ placeholder }}
5 |
6 |
7 |
8 |
9 |
10 |
36 |
--------------------------------------------------------------------------------
/src/components/misc/Transitionable.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
36 |
--------------------------------------------------------------------------------
/src/icons/LoadingIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
16 |
27 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/__tests/components/TRichSelect/RichSelectTriggerTags.spec.ts:
--------------------------------------------------------------------------------
1 | import { NormalizedOption } from '@variantjs/core';
2 | import { shallowMount } from '@vue/test-utils';
3 | import { computed } from 'vue';
4 | import RichSelectTriggerTags from '../../../components/TRichSelect/RichSelectTriggerTags.vue';
5 |
6 | describe('RichSelectTriggerTags', () => {
7 | it('it renders every option', () => {
8 | const selectedOptions = computed(() => [
9 | { value: 'a', text: 'Value A' },
10 | { value: 'b', text: 'Value B' },
11 | { value: 'c', text: 'Value C' },
12 | ]);
13 |
14 | const wrapper = shallowMount(RichSelectTriggerTags, {
15 | global: {
16 | provide: {
17 | selectedOption: selectedOptions,
18 | },
19 | },
20 | });
21 |
22 | expect(wrapper.findAll('rich-select-trigger-tags-tag-stub')).toHaveLength(3);
23 | expect(Object.keys(wrapper.find('rich-select-trigger-tags-tag-stub').attributes())).toEqual(['option']);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/TTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ configuration.text }}
8 |
9 |
10 |
11 |
12 |
44 |
--------------------------------------------------------------------------------
/src/development/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Menú
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
43 |
--------------------------------------------------------------------------------
/src/use/useMultioptions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | flattenOptions,
3 | InputOptions,
4 | NormalizedOption,
5 | NormalizedOptions,
6 | normalizeOptions,
7 | } from '@variantjs/core';
8 | import { computed, ComputedRef, Ref } from 'vue';
9 |
10 | export default function useMultioptions(
11 | options: Ref,
12 | textAttribute: Ref,
13 | valueAttribute: Ref,
14 | normalize: Ref,
15 | ): {
16 | normalizedOptions: ComputedRef
17 | flattenedOptions: ComputedRef
18 | } {
19 | const normalizedOptions = computed(() => {
20 | if (!normalize.value) {
21 | return options.value as NormalizedOptions;
22 | }
23 |
24 | return normalizeOptions(
25 | options.value,
26 | textAttribute.value,
27 | valueAttribute.value,
28 | );
29 | });
30 |
31 | // Flattened array with all posible options
32 | const flattenedOptions = computed((): NormalizedOption[] => flattenOptions(normalizedOptions.value));
33 |
34 | return {
35 | normalizedOptions,
36 | flattenedOptions,
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/src/__tests/use/useInjectsClassesList.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import { defineComponent } from 'vue';
3 | import useInjectsClassesList from '../../use/useInjectsClassesList';
4 |
5 | describe('useInjectsClassesList', () => {
6 | const configurationToProvide = {
7 | classesList: {
8 | test: 'test',
9 | foo: 'bar',
10 | },
11 | };
12 |
13 | const component = defineComponent({
14 | setup() {
15 | const classesList = useInjectsClassesList();
16 |
17 | return { classesList };
18 | },
19 | template: '
',
20 | });
21 |
22 | it('returns the provided configuration option', () => {
23 | const wrapper = shallowMount(component, {
24 | global: {
25 | provide: {
26 | configuration: configurationToProvide,
27 | },
28 | },
29 | });
30 |
31 | expect(wrapper.vm.classesList).toEqual(configurationToProvide.classesList);
32 | });
33 |
34 | it('returns empty object if classeslist are not provided', () => {
35 | const wrapper = shallowMount(component);
36 |
37 | expect(wrapper.vm.classesList).toEqual({});
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/__tests/utils/svgToVueComponent.spec.ts:
--------------------------------------------------------------------------------
1 | import { VNode } from 'vue';
2 | import { svgToVueComponent } from '../../utils/svgToVueComponent';
3 |
4 | describe('svgToVueComponent', () => {
5 | const svg = ` `;
16 |
17 | it('should return a Vnode from an SVG string', () => {
18 | const component: VNode = svgToVueComponent(svg);
19 | expect(component.type).toBe('svg');
20 |
21 | expect(svgToVueComponent(svg).type).toBe('svg');
22 | });
23 |
24 | it('handle html element values', () => {
25 | const div = document.createElement('div');
26 | div.innerHTML = 'Hello World';
27 |
28 | const component: VNode = svgToVueComponent(div);
29 |
30 | expect(component.type).toBe('DIV');
31 | });
32 |
33 | it('handles invalid element values', () => {
34 | const component: VNode = svgToVueComponent('sfsd');
35 |
36 | expect(component.type).toBe('span');
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/__tests/use/useInjectsConfiguration.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import { defineComponent } from 'vue';
3 | import useInjectsConfiguration from '../../use/useInjectsConfiguration';
4 |
5 | describe('useInjectsConfiguration', () => {
6 | const configurationToProvide = {
7 | name: 'test',
8 | foo: 'bar',
9 | classesList: {
10 | test: 'test',
11 | },
12 | };
13 |
14 | const component = defineComponent({
15 | setup() {
16 | const configuration = useInjectsConfiguration();
17 |
18 | return { configuration };
19 | },
20 | template: '
',
21 | });
22 |
23 | it('returns the provided configuration option', () => {
24 | const wrapper = shallowMount(component, {
25 | global: {
26 | provide: {
27 | configuration: configurationToProvide,
28 | },
29 | },
30 | });
31 |
32 | expect(wrapper.vm.configuration).toEqual(configurationToProvide);
33 | });
34 |
35 | it('returns the an empty configuration if no provide', () => {
36 | const wrapper = shallowMount(component);
37 |
38 | expect(wrapper.vm.configuration).toEqual({});
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "plugin:vue/vue3-recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "airbnb-typescript/base"
10 | ],
11 | "parser": "vue-eslint-parser",
12 | "parserOptions": {
13 | "ecmaVersion": 2018,
14 | "parser": "@typescript-eslint/parser",
15 | "sourceType": "module",
16 | "project": "tsconfig.json",
17 | "tsconfigRootDir": "./",
18 | "extraFileExtensions": [ ".vue" ]
19 | },
20 | "plugins": [
21 | "vue",
22 | "@typescript-eslint",
23 | "tree-shaking"
24 | ],
25 | "rules": {
26 | "max-len": "off",
27 | "import/extensions": "off",
28 | "import/prefer-default-export": "off",
29 | "import/no-extraneous-dependencies": "off",
30 | "@typescript-eslint/no-non-null-assertion": "off",
31 | "tree-shaking/no-side-effects-in-initialization": 2
32 | },
33 | "overrides": [
34 | {
35 | "files": ["*.spec.ts", "src/main.ts", "src/development/*"],
36 | "rules": {
37 | "tree-shaking/no-side-effects-in-initialization": "off"
38 | }
39 | }
40 | ]
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/src/components/TRadio.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
39 |
--------------------------------------------------------------------------------
/src/development/AppMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Home
5 |
6 |
7 | Theme
8 |
9 |
10 | Attributes
11 |
12 |
13 | Checkbox
14 |
15 |
16 | Options
17 |
18 |
19 | MultiOptions
20 |
21 |
22 | Dropdown
23 |
24 |
25 | Alert
26 |
27 |
28 | Rich Select
29 |
30 |
31 | Modal
32 |
33 |
34 | Dialog
35 |
36 |
37 | About
38 |
39 |
40 |
41 |
42 |
53 |
--------------------------------------------------------------------------------
/src/__tests/testUtils.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import { Data } from '@variantjs/core';
4 |
5 | export const scopedParamsAsString = (params: Data) : string => {
6 | const keys = Object.keys(params);
7 | const result: Data = {};
8 | keys.filter((key) => key !== 'key').forEach((key) => {
9 | result[key] = typeof params[key];
10 | });
11 | return JSON.stringify(result);
12 | };
13 |
14 | export const parseScopedParams = (paramsAsString: string) : Data => JSON.parse(paramsAsString);
15 |
16 | export const getChildComponentNameByRef = (wrapper: any, refName: string): string | undefined => {
17 | const component = wrapper.vm.$refs[refName];
18 |
19 | return component?.$?.type.name;
20 | };
21 |
22 | export const componentHasAttributeWithValue = (component: any, attributeName: string, attributeValue: any): boolean => component.$.attrs[attributeName] === attributeValue;
23 |
24 | export const componentHasAttributeWithInlineHandlerAndParameter = (component: any, attributeName: string, parameterName: any): boolean => {
25 | const functionAsString: string = component.$.attrs[attributeName].toString();
26 | return functionAsString.includes(parameterName);
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/TCheckbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
44 |
--------------------------------------------------------------------------------
/src/components/TSelect/TSelectOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
14 |
20 |
21 |
22 |
45 |
--------------------------------------------------------------------------------
/src/development/router.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router';
2 |
3 | import Home from './Home.vue';
4 | import About from './About.vue';
5 | import Options from './Options.vue';
6 | import Multioptions from './Multioptions.vue';
7 | import Dropdown from './Dropdown.vue';
8 | import Alert from './Alert.vue';
9 | import Modal from './Modal.vue';
10 | import Dialog from './Dialog.vue';
11 | import Checkbox from './Check.vue';
12 | import Theme from './Theme.vue';
13 | import RichSelect from './RichSelect.vue';
14 | import Attributes from './Attributes.vue';
15 |
16 | const routes = [
17 | { path: '/', component: Home },
18 | { path: '/about', component: About },
19 | { path: '/options', component: Options },
20 | { path: '/multioptions', component: Multioptions },
21 | { path: '/dropdown', component: Dropdown },
22 | { path: '/alert', component: Alert },
23 | { path: '/modal', component: Modal },
24 | { path: '/dialog', component: Dialog },
25 | { path: '/theme', component: Theme },
26 | { path: '/attributes', component: Attributes },
27 | { path: '/checkbox', component: Checkbox },
28 | { path: '/rich-select', component: RichSelect },
29 | ];
30 |
31 | const router = createRouter({
32 | history: createWebHashHistory(),
33 | routes,
34 | });
35 |
36 | export default router;
37 |
--------------------------------------------------------------------------------
/src/use/useConfigurationWithClassesList.ts:
--------------------------------------------------------------------------------
1 | import {
2 | computed, getCurrentInstance, reactive, watch,
3 | } from 'vue';
4 | import { Data, parseVariantWithClassesList } from '@variantjs/core';
5 | import { useAttributes, useConfigurationParts } from './useConfiguration';
6 |
7 | export default function useConfigurationWithClassesList(defaultConfiguration: ComponentOptions, classesListKeys: string[]): {
8 | configuration: ComponentOptions,
9 | attributes: Data,
10 | } {
11 | const vm = getCurrentInstance()!;
12 |
13 | const { propsValues, componentGlobalConfiguration } = useConfigurationParts();
14 |
15 | const computedConfiguration = computed(() => ({
16 | ...vm.props,
17 | ...parseVariantWithClassesList(
18 | propsValues.value,
19 | classesListKeys,
20 | componentGlobalConfiguration,
21 | defaultConfiguration,
22 | ),
23 | }));
24 |
25 | const configuration = reactive(computedConfiguration.value);
26 |
27 | watch(computedConfiguration, (newValue) => {
28 | Object.keys(newValue).forEach((key) => {
29 | configuration[key] = newValue[key];
30 | });
31 | });
32 |
33 | const attributes = useAttributes(configuration);
34 |
35 | return {
36 | configuration: configuration as ComponentOptions,
37 | attributes,
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/src/__tests/utils/popper.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import { ModifierArguments } from '@popperjs/core';
4 | import { Data } from '@variantjs/core';
5 | import { sameWidthModifier } from '../../utils/popper';
6 |
7 | describe('sameWidthModifier', () => {
8 | it('sets the popper width from the reference width', () => {
9 | const options = {
10 | state: {
11 | styles: {
12 | popper: {},
13 | },
14 | rects: {
15 | reference: {
16 | width: 100,
17 | },
18 | },
19 | },
20 |
21 | } as any as ModifierArguments;
22 |
23 | sameWidthModifier.fn(options);
24 |
25 | expect(options.state.styles.popper.width).toBe('100px');
26 | });
27 |
28 | it('sets the popper width from the reference width on `effect` function', () => {
29 | const options = {
30 | state: {
31 | elements: {
32 | popper: {
33 | style: {},
34 | },
35 | reference: {
36 | offsetWidth: 100,
37 | },
38 | },
39 | },
40 |
41 | } as any as ModifierArguments;
42 |
43 | sameWidthModifier.effect!(options);
44 |
45 | expect(options.state.elements.popper.style.width).toBe('100px');
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/use/useMulipleableVModel.ts:
--------------------------------------------------------------------------------
1 | import { Data } from '@variantjs/core';
2 | import {
3 | computed, ref, getCurrentInstance, Ref, watch,
4 | } from 'vue';
5 |
6 | export default function useMulipleableVModel(
7 | props: P,
8 | key: K,
9 | configuration?: C,
10 | ): {
11 | localValue: Ref
;
12 | clearValue: () => void
13 | } {
14 | const vm = getCurrentInstance();
15 |
16 | const isMultiple = computed((): boolean => (configuration === undefined ? false : configuration.multiple !== null && configuration.multiple !== undefined && configuration.multiple !== false));
17 |
18 | const getDefaultValue = (): P[K] => {
19 | if (isMultiple.value) {
20 | return [] as P[K];
21 | }
22 |
23 | return undefined as P[K];
24 | };
25 |
26 | const initialValue = props[key];
27 |
28 | const localValue = ref(initialValue === undefined ? getDefaultValue() : initialValue) as Ref;
29 |
30 | watch(localValue, (value) => {
31 | vm?.emit(`update:${key}`, value);
32 | });
33 |
34 | watch(() => props[key], (value) => {
35 | localValue.value = value;
36 | });
37 |
38 | const clearValue = () : void => {
39 | localValue.value = getDefaultValue();
40 | };
41 |
42 | watch(isMultiple, () => {
43 | clearValue();
44 | });
45 |
46 | return {
47 | localValue,
48 | clearValue,
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/TInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 |
13 |
50 |
--------------------------------------------------------------------------------
/src/components/TTextarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 |
13 |
49 |
--------------------------------------------------------------------------------
/src/utils/svgToVueComponent.ts:
--------------------------------------------------------------------------------
1 | import { Data } from '@variantjs/core';
2 | import { h, VNode, VNodeProps } from 'vue';
3 |
4 | const icons: {
5 | [key: string]: VNode
6 | } = {};
7 |
8 | export const svgToVueComponent = (el: Element | string, deep = 0): VNode => {
9 | let iconAsString: string | null = null;
10 |
11 | if (deep === 0) {
12 | iconAsString = typeof el === 'string' ? el : el.outerHTML;
13 |
14 | if (icons[iconAsString]) {
15 | return icons[iconAsString];
16 | }
17 | }
18 |
19 | let elToConvert: Element | null | string = el;
20 |
21 | if (typeof elToConvert === 'string') {
22 | const div = document.createElement('div');
23 | div.innerHTML = elToConvert;
24 | elToConvert = div.firstElementChild;
25 | }
26 |
27 | if (elToConvert === null) {
28 | return h('span');
29 | }
30 |
31 | const attributes = Array.from(elToConvert.attributes);
32 | const children = Array.from(elToConvert.children);
33 | const attrs: VNodeProps & Data = {};
34 |
35 | attributes
36 | .filter((attribute: Attr) => !attribute.name.startsWith('on'))
37 | .forEach((attribute: Attr) => {
38 | attrs[attribute.name] = attribute.value;
39 | });
40 |
41 | const component = h(elToConvert.tagName, attrs, children.map((child) => svgToVueComponent(child, deep + 1)));
42 |
43 | if (deep === 0 && iconAsString !== null) {
44 | icons[iconAsString] = component;
45 | }
46 |
47 | return component;
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectTriggerTags.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
51 |
--------------------------------------------------------------------------------
/src/utils/emitter.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { EmitterEvents, EmitterFunction, EmitterInterface } from '../types';
3 |
4 | export class Emitter implements EmitterInterface {
5 | /* eslint-disable tree-shaking/no-side-effects-in-initialization */
6 | private events: EmitterEvents = {};
7 |
8 | on(name: keyof EmitterEvents, callback: EmitterFunction): void {
9 | if (this.events[name] === undefined) {
10 | this.events[name] = [callback];
11 | } else {
12 | this.events[name].push(callback);
13 | }
14 | }
15 |
16 | once(name: keyof EmitterEvents, callback: EmitterFunction): void {
17 | const listener = (...args: any[]) => {
18 | callback(...args);
19 | this.off(name, listener);
20 | };
21 |
22 | return this.on(name, listener);
23 | }
24 |
25 | emit(name: keyof EmitterEvents, ...args: any[]): void {
26 | const events = this.events[name];
27 |
28 | if (events === undefined) {
29 | return;
30 | }
31 |
32 | events.forEach((callback) => {
33 | callback(...args);
34 | });
35 | }
36 |
37 | off(name: keyof EmitterEvents, callback: EmitterFunction): void {
38 | const events = this.events[name];
39 |
40 | if (events === undefined) {
41 | return;
42 | }
43 |
44 | const index = events.findIndex((c) => c === callback);
45 |
46 | if (index < 0) {
47 | return;
48 | }
49 |
50 | events.splice(index, 1);
51 | this.events[name] = events;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export { VariantJSConfiguration, VariantJSProps, VariantJSWithClassesListProps } from './variantCore';
2 | export { TInputValue, TInputOptions } from './components/t-input';
3 | export { TTextareaValue, TTextareaOptions } from './components/t-textarea';
4 | export { TRadioValue, TRadioOptions } from './components/t-radio';
5 | export { TCheckboxValue, TCheckboxOptions } from './components/t-checkbox';
6 | export { TSelectValue, TSelectOptions } from './components/t-select';
7 | export { TButtonOptions } from './components/t-button';
8 | export { TCardOptions } from './components/t-card';
9 | export { TModalOptions } from './components/t-modal';
10 | export { TDialogOptions } from './components/t-dialog';
11 | export { TTagOptions } from './components/t-tag';
12 | export { TAlertOptions } from './components/t-alert';
13 | export { TToggleValue, TToggleOptions } from './components/t-toggle';
14 | export { TDropdownOptions } from './components/t-dropdown';
15 | export { TRichSelectOptions, MinimumInputLengthTextProp } from './components/t-rich-select';
16 | export { TInputGroupOptions, TInputGroupValidChilElementsKeys } from './components/t-input-group';
17 | export {
18 | Truthy, IconProp, FetchOptionsFn, FetchedOptions, PreFetchOptionsFn, PromiseRejectFn,
19 | } from './misc';
20 | export { ObjectWithProperties, KeysOfType } from './helpers';
21 | export { EmitterEvents, EmitterFunction, EmitterInterface } from './utils';
22 | export { VueRouteAriaCurrentValue, VueRouteRouteLocationRaw } from './vueRouter';
23 |
--------------------------------------------------------------------------------
/src/__tests/use/useSetup.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable vue/one-component-per-file */
2 | import {
3 | defineComponent, createApp, h, ComponentPropsOptions,
4 | } from 'vue';
5 | import { variantJS } from '../..';
6 | import { VariantJSConfiguration } from '../../types';
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
9 | type InstanceType = V extends { new (...arg: any[]): infer X } ? X : never;
10 |
11 | type VM = InstanceType & { unmount(): void };
12 |
13 | export function mount(Comp: V, attributes?: Record, configuration?: VariantJSConfiguration): VM {
14 | const el = document.createElement('div');
15 | const app = createApp(Comp, attributes);
16 |
17 | app.use(variantJS, configuration);
18 |
19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
20 | return app.mount(el) as any as VM;
21 | }
22 |
23 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
24 | export function useSetup(
25 | setup: () => V,
26 | configuration?: VariantJSConfiguration,
27 | attributes?: Record,
28 | props: ComponentPropsOptions = {},
29 | componentName: keyof VariantJSConfiguration = 'TInput',
30 | ) {
31 | const componentOptions = {
32 | name: componentName,
33 | props,
34 | setup,
35 | render() {
36 | return h('div', []);
37 | },
38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
39 | } as any;
40 |
41 | const Comp = defineComponent(componentOptions);
42 |
43 | return mount(Comp, attributes, configuration);
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/getVariantProps.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CSSClass, CSSRawClassesList, Data, Variants, VariantsWithClassesList,
3 | } from '@variantjs/core';
4 |
5 | import { PropType } from 'vue';
6 | import { VariantJSProps, VariantJSWithClassesListProps } from '../types';
7 |
8 | const getVariantProps = () : VariantJSProps => ({
9 | classes: {
10 | type: [String, Array, Object] as PropType,
11 | default: undefined,
12 | },
13 | fixedClasses: {
14 | type: [String, Array, Object] as PropType,
15 | default: undefined,
16 | },
17 | variants: {
18 | type: Object as PropType>,
19 | default: undefined,
20 | },
21 | variant: {
22 | type: String as PropType,
23 | default: undefined,
24 | },
25 | });
26 |
27 | const getVariantPropsWithClassesList = () : VariantJSWithClassesListProps => ({
28 | classes: {
29 | type: [String, Array, Object] as PropType>,
30 | default: undefined,
31 | },
32 | fixedClasses: {
33 | type: [String, Array, Object] as PropType>,
34 | default: undefined,
35 | },
36 | variants: {
37 | type: Object as PropType>,
38 | default: undefined,
39 | },
40 | variant: {
41 | type: String as PropType,
42 | default: undefined,
43 | },
44 | });
45 |
46 | export { getVariantProps, getVariantPropsWithClassesList };
47 |
--------------------------------------------------------------------------------
/src/development/Attributes.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
Gets the same placeholder attribute from different sources
8 |
9 |
From the attribute
10 |
14 |
15 |
Placeholder from the value (v-model) of input above
16 |
17 |
18 |
From the attribute (to ensure is overriden)
19 |
23 |
24 |
Placeholder on the configuration
25 |
26 |
27 |
Placeholder on the variant
28 |
29 |
30 |
31 |
32 |
33 |
64 |
--------------------------------------------------------------------------------
/src/__tests/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as library from '../index';
2 |
3 | describe('main file', () => {
4 | it('provides all the needed data', () => {
5 | expect(Object.keys(library)).toEqual([
6 | 'TInput',
7 | 'TButton',
8 | 'TTextarea',
9 | 'TSelect',
10 | 'TCheckbox',
11 | 'TRadio',
12 | 'TInputGroup',
13 | 'TRichSelect',
14 | 'TTag',
15 | 'TCard',
16 | 'TDropdown',
17 | 'TAlert',
18 | 'TModal',
19 | 'TDialog',
20 | 'TToggle',
21 | 'variantJS',
22 | 'LoadingIcon',
23 | 'Emitter',
24 | 'getVariantProps',
25 | 'getVariantPropsWithClassesList',
26 | 'sameWidthModifier',
27 | 'svgToVueComponent',
28 | 'useActivableOption',
29 | 'useConfiguration',
30 | 'useConfigurationWithClassesList',
31 | 'useFetchsOptions',
32 | 'useInjectsClassesList',
33 | 'useInjectsClassesListClass',
34 | 'useInjectsConfiguration',
35 | 'useMulipleableVModel',
36 | 'useMultioptions',
37 | 'useSelectableOption',
38 | 'useVModel',
39 | ]);
40 | });
41 |
42 | describe('callable utils', () => {
43 | it('can create an instance of emitter', () => {
44 | const emitter = new library.Emitter();
45 |
46 | expect(emitter).toBeInstanceOf(library.Emitter);
47 | });
48 |
49 | it('have functions', () => {
50 | expect(typeof library.getVariantProps).toBe('function');
51 | expect(typeof library.getVariantPropsWithClassesList).toBe('function');
52 | expect(typeof library.sameWidthModifier).toBe('object');
53 | expect(typeof library.svgToVueComponent).toBe('function');
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/utils/createDialogProgramatically.ts:
--------------------------------------------------------------------------------
1 | import { DialogResponse, DialogType } from '@variantjs/core';
2 | import { createApp } from 'vue';
3 | import { TDialogOptions } from '../types/components/t-dialog';
4 | import { VariantJSConfiguration } from '../types/variantCore';
5 |
6 | import TDialog from '../components/TDialog.vue';
7 |
8 | const createDialogProgramatically = (configuration: VariantJSConfiguration, type: DialogType, titleOrDialogOptions: TDialogOptions | string, text?: string, icon?: string) : Promise => {
9 | const { props } = TDialog;
10 |
11 | if (typeof titleOrDialogOptions === 'string') {
12 | props.title.default = titleOrDialogOptions;
13 | } else {
14 | Object.keys(titleOrDialogOptions).forEach((key) => {
15 | props[key].default = titleOrDialogOptions[key];
16 | });
17 | }
18 |
19 | if (typeof text === 'string') {
20 | props.text.default = text;
21 | }
22 |
23 | if (typeof icon === 'string') {
24 | props.icon.default = icon;
25 | }
26 |
27 | props.type.default = type;
28 |
29 | const instance = createApp(TDialog);
30 |
31 | instance.provide('configuration', configuration);
32 |
33 | const dialogInstance = instance.mount(document.createElement('div'));
34 |
35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
36 | const promise = (dialogInstance as any).show() as Promise;
37 |
38 | return promise
39 | .then((response) => {
40 | instance.unmount();
41 |
42 | return Promise.resolve(response);
43 | })
44 | .catch((error) => {
45 | instance.unmount();
46 |
47 | return Promise.reject(error);
48 | });
49 | };
50 |
51 | export default createDialogProgramatically;
52 |
--------------------------------------------------------------------------------
/src/types/components/t-dialog.ts:
--------------------------------------------------------------------------------
1 | import {
2 | WithVariantPropsAndClassesList, Data, TDialogClassesValidKeys, DialogPreconfirmFn, DialogInputValidatorFn,
3 | } from '@variantjs/core';
4 | import { BodyScrollOptions } from 'body-scroll-lock';
5 | import { HTMLAttributes } from 'vue';
6 |
7 | export type TDialogOptions = WithVariantPropsAndClassesList<{
8 | type?: string,
9 | icon?: string,
10 | useSolidIcon?: boolean,
11 | rejectOnCancel?: boolean,
12 | rejectOnDismiss?: boolean,
13 | title?: string,
14 | titleTag?: string,
15 | text?: string,
16 | textTag?: string,
17 | cancelButtonText?: string,
18 | cancelButtonAriaLabel?: string,
19 | okButtonText?: string,
20 | okButtonAriaLabel?: string,
21 | preConfirm?: DialogPreconfirmFn,
22 | name?: string,
23 | modelValue?: boolean,
24 | dialogAttributes?: HTMLAttributes & Data,
25 | tagName?: string
26 | clickToClose?: boolean,
27 | escToClose?: boolean,
28 | focusOnOpen?: boolean,
29 | showCloseButton?: boolean,
30 | disableBodyScroll?: boolean,
31 | bodyScrollLockOptions?: BodyScrollOptions,
32 | teleport?: boolean,
33 | teleportTo?: string | HTMLElement,
34 | // (Prompt only)
35 | // Attributes for the text input
36 | inputAttributes?: HTMLAttributes & Data,
37 | // Type for the prompt input (accepts 'input', 'textarea' ,'select' and 'checkbox')
38 | inputType?: 'string',
39 | // Function for validate the value of the prompt, receives the prompt value and should return an error message or empty if no errors. It accepts a promise
40 | inputValidator?: DialogInputValidatorFn,
41 | // Default value of the input
42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
43 | inputValue?: any,
44 | } & HTMLAttributes & Data, TDialogClassesValidKeys>;
45 |
--------------------------------------------------------------------------------
/src/types/components/t-rich-select.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Data, InputOptions, Measure, NormalizedOption, NormalizedOptions, TRichSelectClassesValidKeys, WithVariantPropsAndClassesList,
3 | } from '@variantjs/core';
4 | import { HTMLAttributes } from 'vue';
5 | import { Placement, Options } from '@popperjs/core';
6 | import { TSelectValue } from './t-select';
7 | import { FetchOptionsFn, PreFetchOptionsFn } from '../misc';
8 |
9 | export type MinimumInputLengthTextProp = ((minimumInputLength: number, query?: string) => string) | string;
10 |
11 | export type TRichSelectOptions = WithVariantPropsAndClassesList<{
12 | modelValue?: TSelectValue,
13 | options?: InputOptions | NormalizedOption[] | NormalizedOptions,
14 | multiple?: boolean
15 | name?: string,
16 | tags?: boolean
17 | normalizeOptions?: boolean,
18 | valueAttribute?: string,
19 | textAttribute?: string,
20 | delay?: number,
21 | fetchOptions?: FetchOptionsFn,
22 | prefetchOptions?: boolean | PreFetchOptionsFn,
23 | minimumInputLength?: number,
24 | minimumInputLengthText?: MinimumInputLengthTextProp,
25 | minimumResultsForSearch?: number,
26 | hideSearchBox?: boolean,
27 | toggleOnFocus?: boolean,
28 | toggleOnClick?: boolean,
29 | closeOnSelect?: boolean,
30 | selectOnClose?: boolean,
31 | clearable?: boolean,
32 | disabled?: boolean,
33 | placeholder?: string,
34 | searchBoxPlaceholder?: string,
35 | noResultsText?: string,
36 | searchingText?: string,
37 | loadingClosedPlaceholder?: string,
38 | loadingMoreResultsText?: string,
39 | maxHeight?: Measure | null,
40 | dropdownPlacement?: Placement,
41 | dropdownPopperOptions?: Options,
42 | teleport?: boolean,
43 | teleportTo?: string | HTMLElement,
44 | } & HTMLAttributes & Data, TRichSelectClassesValidKeys>;
45 |
--------------------------------------------------------------------------------
/src/__tests/components/icons/CustomIcon.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils';
2 | import CustomIcon from '@/icons/CustomIcon.vue';
3 | import CloseIcon from '@/icons/CloseIcon.vue';
4 |
5 | describe('CustomIcon.vue', () => {
6 | it('accepts a SVG string as a closeIcon', async () => {
7 | const icon = `
8 |
9 | `;
10 | const wrapper = mount(CustomIcon, {
11 | props: {
12 | icon,
13 | },
14 | });
15 |
16 | expect(wrapper.vm.$el.innerHTML).toContain(' {
20 | const icon = `
21 |
22 | `;
23 | const wrapper = mount(CustomIcon, {
24 | props: {
25 | icon,
26 | },
27 | });
28 |
29 | expect(wrapper.vm.$el.innerHTML).not.toContain('document.cookie');
30 | });
31 |
32 | it('accepts another vue component as a custom icon', async () => {
33 | // Supress Vue received a Component which was made a reactive object.This can lead to unnecessary performance overhead, and should be avoided by marking the component with `markRaw` or using `shallowRef` instead of `ref
34 | // @TODO consider an alternative to this
35 | jest.spyOn(console, 'warn').mockImplementation(() => {});
36 |
37 | const wrapper = mount(CustomIcon, {
38 | props: {
39 | icon: CloseIcon,
40 | },
41 | });
42 |
43 | expect(wrapper.vm.$el.innerHTML).toContain('M6 18L18 6M6 6l12 12');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/development/Multioptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
Multioptions components v-model sync
7 |
12 |
13 |
19 |
20 |
27 |
28 |
29 |
30 |
31 |
80 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectState.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
18 |
23 |
24 |
25 |
26 |
60 |
--------------------------------------------------------------------------------
/src/development/Alert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Alert
5 |
6 |
7 |
11 |
12 | Lorem, ipsum dolor sit amet consectetur adipisicing elit. Obcaecati ipsam necessitatibus deserunt quas dolorum at laboriosam, expedita eveniet facere excepturi non hic esse! Facere, illum qui? Minus iste porro quidem!
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
29 | Lorem, ipsum dolor sit amet consectetur adipisicing elit. Obcaecati ipsam necessitatibus deserunt quas dolorum at laboriosam, expedita eveniet facere excepturi non hic esse! Facere, illum qui? Minus iste porro quidem!
30 |
31 |
32 |
33 |
34 |
35 |
62 |
--------------------------------------------------------------------------------
/src/__tests/components/TRichSelect/RichSelectDropdown.spec.ts:
--------------------------------------------------------------------------------
1 | import { NormalizedOptions } from '@variantjs/core';
2 | import { shallowMount } from '@vue/test-utils';
3 | import { computed } from 'vue';
4 | import RichSelectDropdown from '../../../components/TRichSelect/RichSelectDropdown.vue';
5 | import { getChildComponentNameByRef } from '../../testUtils';
6 |
7 | describe('RichSelectDropdown', () => {
8 | const options: NormalizedOptions = [{
9 | value: 'a',
10 | text: 'a',
11 | }];
12 |
13 | const showSearchInput = computed(() => true);
14 |
15 | it('renders the component', () => {
16 | const wrapper = shallowMount(RichSelectDropdown, {
17 | global: {
18 | provide: {
19 | options,
20 | showSearchInput,
21 | },
22 | },
23 | });
24 |
25 | expect(wrapper.vm.$el.tagName).toBe('DIV');
26 | });
27 |
28 | it('has a RichSelectOptionsList component', () => {
29 | const wrapper = shallowMount(RichSelectDropdown, {
30 | global: {
31 | provide: {
32 | showSearchInput,
33 | options,
34 | },
35 | },
36 | });
37 |
38 | expect(getChildComponentNameByRef(wrapper, 'optionsList')).toEqual('RichSelectOptionsList');
39 | });
40 |
41 | it('has a RichSelectSearchInput component', () => {
42 | const wrapper = shallowMount(RichSelectDropdown, {
43 | global: {
44 | provide: {
45 | showSearchInput,
46 | options,
47 | },
48 | },
49 | });
50 |
51 | expect(getChildComponentNameByRef(wrapper, 'searchInput')).toEqual('RichSelectSearchInput');
52 | });
53 |
54 | it('hides the RichSelectSearchInput component if `showSearchInput` is `false`', () => {
55 | const wrapper = shallowMount(RichSelectDropdown, {
56 | global: {
57 | provide: {
58 | options,
59 | showSearchInput: computed(() => false),
60 | },
61 | },
62 | });
63 |
64 | expect(getChildComponentNameByRef(wrapper, 'searchInput')).toBeUndefined();
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/src/__tests/utils/emitter.spec.ts:
--------------------------------------------------------------------------------
1 | import { Emitter } from '../../utils/emitter';
2 |
3 | describe('Emitter', () => {
4 | let emitter: Emitter;
5 |
6 | beforeEach(() => {
7 | emitter = new Emitter();
8 | });
9 |
10 | it('calls the function every time when a registered event is called every', () => {
11 | const eventFn = jest.fn();
12 | emitter.on('event-name', eventFn);
13 | expect(eventFn).not.toHaveBeenCalled();
14 | emitter.emit('event-name');
15 | expect(eventFn).toHaveBeenCalledTimes(1);
16 | emitter.emit('event-name');
17 | expect(eventFn).toHaveBeenCalledTimes(2);
18 | });
19 |
20 | it('calls the function only once on an event that is registered once', () => {
21 | const eventFn = jest.fn();
22 | emitter.once('event-name', eventFn);
23 | expect(eventFn).not.toHaveBeenCalled();
24 | emitter.emit('event-name');
25 | emitter.emit('event-name');
26 | expect(eventFn).toHaveBeenCalledTimes(1);
27 | });
28 |
29 | it('stops running a method when calling the off method', () => {
30 | const eventFn = jest.fn();
31 | const eventFn2 = jest.fn();
32 | emitter.on('event-name', eventFn);
33 | emitter.on('event-name', eventFn2);
34 | emitter.emit('event-name');
35 | expect(eventFn).toHaveBeenCalled();
36 | expect(eventFn2).toHaveBeenCalled();
37 | emitter.off('event-name', eventFn2);
38 |
39 | emitter.emit('event-name');
40 | expect(eventFn).toHaveBeenCalledTimes(2);
41 | expect(eventFn2).toHaveBeenCalledTimes(1);
42 | });
43 |
44 | it('handles unexisting events when emitting', () => {
45 | expect(() => {
46 | emitter.emit('event-name');
47 | }).not.toThrowError();
48 | });
49 |
50 | it('handles unexisting events when removing', () => {
51 | expect(() => {
52 | emitter.off('event-name', () => {});
53 | }).not.toThrowError();
54 | });
55 |
56 | it('handles unexisting events methods when removing', () => {
57 | expect(() => {
58 | emitter.on('event-name', () => {});
59 | emitter.off('event-name', () => {});
60 | }).not.toThrowError();
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectSearchInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
17 |
62 |
--------------------------------------------------------------------------------
/src/types/vueRouter.ts:
--------------------------------------------------------------------------------
1 | // Copied from the types on vue/node_modules/vue-router/dist/vue-router.d.ts
2 | // so we dont need to add any extra dependency
3 |
4 | type RouteParamValue = string;
5 |
6 | type RouteParamValueRaw = RouteParamValue | number;
7 |
8 | type RouteParamsRaw = Record;
9 |
10 | type RouteRecordName = string | symbol;
11 |
12 | interface LocationAsRelativeRaw {
13 | name?: RouteRecordName;
14 | params?: RouteParamsRaw;
15 | }
16 |
17 | type HistoryStateArray = Array;
18 |
19 | type HistoryStateValue = string | number | boolean | null | undefined | HistoryState | HistoryStateArray;
20 |
21 | /**
22 | * Allowed HTML history.state
23 | */
24 | interface HistoryState {
25 | [x: number]: HistoryStateValue;
26 | [x: string]: HistoryStateValue;
27 | }
28 |
29 | interface RouteLocationOptions {
30 | /**
31 | * Replace the entry in the history instead of pushing a new entry
32 | */
33 | replace?: boolean;
34 | /**
35 | * Triggers the navigation even if the location is the same as the current one
36 | */
37 | force?: boolean;
38 | /**
39 | * State to save using the History API. This cannot contain any reactive
40 | * values and some primitives like Symbols are forbidden. More info at
41 | * https://developer.mozilla.org/en-US/docs/Web/API/History/state
42 | */
43 | state?: HistoryState;
44 | }
45 |
46 | interface LocationAsPath {
47 | path: string;
48 | }
49 |
50 | type LocationQueryValue = string | null;
51 |
52 | type LocationQueryValueRaw = LocationQueryValue | number | undefined;
53 |
54 | type LocationQueryRaw = Record;
55 |
56 | interface RouteQueryAndHash {
57 | query?: LocationQueryRaw;
58 | hash?: string;
59 | }
60 |
61 | type VueRouteRouteLocationRaw = string | (RouteQueryAndHash & LocationAsPath & RouteLocationOptions) | (RouteQueryAndHash & LocationAsRelativeRaw & RouteLocationOptions);
62 | type VueRouteAriaCurrentValue = 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | undefined;
63 |
64 | export { VueRouteAriaCurrentValue, VueRouteRouteLocationRaw };
65 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectDropdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
23 |
24 |
28 |
29 |
30 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
80 |
--------------------------------------------------------------------------------
/src/__tests/components/TSelect.integration.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { render, fireEvent } from '@testing-library/vue';
3 | import TSelect from '@/components/TSelect.vue';
4 |
5 | describe('TSelect.vue', () => {
6 | it('handles the v-model', async () => {
7 | const { container, getByDisplayValue } = render(TSelect, {
8 | props: {
9 | options: ['A', 'B'],
10 | },
11 | });
12 |
13 | const select = container.querySelector('select')!;
14 |
15 | await fireEvent.update(select!, 'B');
16 |
17 | getByDisplayValue('B');
18 | });
19 |
20 | it('selects the option', async () => {
21 | const { container } = render(TSelect, {
22 | props: {
23 | options: ['A', 'B'],
24 | },
25 | });
26 |
27 | const select = container.querySelector('select')!;
28 |
29 | expect(select.querySelectorAll('option')).toHaveLength(2);
30 | });
31 |
32 | it('contains the class + classes + fixedClasses', async () => {
33 | const { container } = render(TSelect, {
34 | props: {
35 | fixedClasses: 'text-red-500',
36 | classes: 'border-red-500',
37 | class: 'font-semibold',
38 | },
39 | });
40 |
41 | const select = container.querySelector('.text-red-500.border-red-500.font-semibold');
42 | expect(select).not.toBeNull();
43 | });
44 |
45 | it('adds the html attributes', async () => {
46 | const { getByRole, getByTitle } = render(TSelect, {
47 | props: {
48 | role: 'text-field',
49 | title: 'my title',
50 | },
51 | });
52 |
53 | getByRole('text-field');
54 |
55 | getByTitle('my title');
56 | });
57 |
58 | it('adds the classes on the variant', async () => {
59 | const { container } = render(TSelect, {
60 | props: {
61 | variants: {
62 | error: {
63 | classes: 'text-red-500',
64 | },
65 | },
66 | variant: 'error',
67 | classes: 'text-blue-500',
68 | },
69 | });
70 |
71 | let select = container.querySelector('.text-red-500');
72 | expect(select).not.toBeNull();
73 |
74 | select = container.querySelector('.text-blue-500');
75 | expect(select).toBeNull();
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/src/components/TCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
16 | {{ configuration.header }}
17 |
18 |
19 |
20 |
25 |
26 | {{ configuration.body }}
27 |
28 |
29 |
30 |
35 |
39 | {{ configuration.footer }}
40 |
41 |
42 |
43 |
44 |
45 |
85 |
--------------------------------------------------------------------------------
/src/__tests/components/misc/TextPlaceholder.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import TextPlaceholder from '../../../components/misc/TextPlaceholder.vue';
3 |
4 | describe('TextPlaceholder', () => {
5 | it('renders a blank space if no TextPlaceholder is set', () => {
6 | const wrapper = shallowMount(TextPlaceholder);
7 |
8 | expect(wrapper.vm.$el.innerHTML).toEqual(' ');
9 | });
10 |
11 | it('uses the text inside the default prop', () => {
12 | const wrapper = shallowMount(TextPlaceholder, {
13 | slots: {
14 | default: 'Select an option',
15 | },
16 | });
17 |
18 | expect(wrapper.vm.$el.innerHTML).toEqual('Select an option');
19 | });
20 |
21 | it('uses `placeholder` as the property from the `classesList` by default', () => {
22 | const configuration = {
23 | classesList: { placeholder: 'text-red-500' },
24 | };
25 |
26 | const wrapper = shallowMount(TextPlaceholder, {
27 | global: {
28 | provide: {
29 | configuration,
30 | },
31 | },
32 | });
33 |
34 | expect(wrapper.vm.$el.className).toEqual('text-red-500');
35 | });
36 |
37 | it('accepts a different property for the `classesList` by object', () => {
38 | const configuration = {
39 | classesList: { buttonPlaceholder: 'text-red-500' },
40 | };
41 | const wrapper = shallowMount(TextPlaceholder, {
42 | global: {
43 | provide: {
44 | configuration,
45 | },
46 | },
47 | props: {
48 | classProperty: 'buttonPlaceholder',
49 | },
50 | });
51 |
52 | expect(wrapper.vm.$el.className).toEqual('text-red-500');
53 | });
54 |
55 | it('uses the prop placeholder if set', () => {
56 | const wrapper = shallowMount(TextPlaceholder, {
57 | props: {
58 | placeholder: 'Select an option',
59 | },
60 | });
61 |
62 | expect(wrapper.vm.$el.innerHTML).toEqual('Select an option');
63 | });
64 |
65 | it('prioritized the slot over the placeholder attribute', () => {
66 | const wrapper = shallowMount(TextPlaceholder, {
67 | props: {
68 | placeholder: 'Something else',
69 | },
70 | slots: {
71 | default: 'Select an option',
72 | },
73 | });
74 |
75 | expect(wrapper.vm.$el.innerHTML).toEqual('Select an option');
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/src/__tests/components/TTag.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import TTag from '@/components/TTag.vue';
3 |
4 | describe('TTag.vue', () => {
5 | it('renders the component without errors', () => {
6 | const wrapper = shallowMount(TTag);
7 | expect(wrapper.vm.$el.tagName).toBe('DIV');
8 | });
9 |
10 | it('accepts a different tag for the wrapper', () => {
11 | const wrapper = shallowMount(TTag, {
12 | props: {
13 | tagName: 'table',
14 | },
15 | });
16 | expect(wrapper.vm.$el.tagName).toBe('TABLE');
17 | });
18 |
19 | it('renders the default slot content', () => {
20 | const wrapper = shallowMount(TTag, {
21 | slots: {
22 | default: 'Im a tag!',
23 | },
24 | });
25 |
26 | expect(wrapper.vm.$el.innerHTML).toBe('Im a tag!');
27 | });
28 |
29 | it('prioritizes slot over test prop', () => {
30 | const wrapper = shallowMount(TTag, {
31 | props: {
32 | text: 'Im a tag!',
33 | },
34 | slots: {
35 | default: 'default slot',
36 | },
37 | });
38 |
39 | expect(wrapper.vm.$el.innerHTML).toBe('default slot');
40 | });
41 |
42 | it('adds the attributes', () => {
43 | const wrapper = shallowMount(TTag, {
44 | slots: {
45 | default: 'default slot',
46 | },
47 | attrs: {
48 | id: 'my-id',
49 | },
50 | });
51 |
52 | expect(wrapper.html()).toBe('default slot
');
53 | });
54 |
55 | it('adds the attributes from the configuration', () => {
56 | const wrapper = shallowMount(TTag, {
57 | global: {
58 | provide: {
59 | configuration: {
60 | TTag: {
61 | 'custom-attribute': 'Hello World!',
62 | },
63 | },
64 | },
65 | },
66 | });
67 |
68 | expect(wrapper.html()).toBe('
');
69 | });
70 |
71 | it('used the props from global configuration', () => {
72 | const wrapper = shallowMount(TTag, {
73 | global: {
74 | provide: {
75 | configuration: {
76 | TTag: {
77 | tagName: 'table',
78 | text: 'Copyright @alfonsobires',
79 | },
80 | },
81 | },
82 | },
83 | });
84 |
85 | expect(wrapper.vm.$el.tagName).toBe('TABLE');
86 | expect(wrapper.vm.$el.innerHTML).toBe('Copyright @alfonsobires');
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/src/components/TSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
15 |
85 |
--------------------------------------------------------------------------------
/src/__tests/components/TSelect/TSelectOption.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { mount, shallowMount } from '@vue/test-utils';
3 | import { NormalizedOption } from '@variantjs/core';
4 | import TSelectOption from '@/components/TSelect/TSelectOption.vue';
5 |
6 | describe('TSelectOption.vue', () => {
7 | it('renders the option', () => {
8 | const option: NormalizedOption = {
9 | text: 'Hello World',
10 | value: 'hello',
11 | };
12 | const wrapper = shallowMount(TSelectOption, {
13 | props: {
14 | option,
15 | },
16 | });
17 | expect(wrapper.html()).toBe('Hello World ');
18 | });
19 | it('renders an optgroup if has children elements', () => {
20 | const option: NormalizedOption = {
21 | text: 'Hello World',
22 | value: 'hello',
23 | children: [{
24 | text: 'Letter A', value: 'A',
25 | }],
26 | };
27 | const wrapper = mount(TSelectOption, {
28 | props: {
29 | option,
30 | },
31 | });
32 |
33 | const optGroup = wrapper.vm.$el;
34 | const options = optGroup.querySelectorAll(['option']);
35 | expect(optGroup.tagName).toBe('OPTGROUP');
36 | expect(options).toHaveLength(1);
37 | expect(options[0].value).toBe('A');
38 | expect((options[0] as HTMLOptionElement).text).toBe('Letter A');
39 | });
40 |
41 | it('disables optgroup ', () => {
42 | const option: NormalizedOption = {
43 | text: 'Hello World',
44 | value: 'hello',
45 | children: [{
46 | text: 'A', value: 'A',
47 | }],
48 | disabled: true,
49 | };
50 | const wrapper = shallowMount(TSelectOption, {
51 | props: {
52 | option,
53 | },
54 | });
55 |
56 | expect(wrapper.vm.$el.disabled).toBeTruthy();
57 | });
58 |
59 | it('disables the option', () => {
60 | const option: NormalizedOption = {
61 | text: 'Hello World',
62 | value: 'hello',
63 | disabled: true,
64 | };
65 | const wrapper = shallowMount(TSelectOption, {
66 | props: {
67 | option,
68 | },
69 | });
70 | expect(wrapper.html()).toBe('Hello World ');
71 | });
72 |
73 | it('disables the option with a `disabled` string', () => {
74 | const option: NormalizedOption = {
75 | text: 'Hello World',
76 | value: 'hello',
77 | disabled: 'disabled',
78 | };
79 | const wrapper = shallowMount(TSelectOption, {
80 | props: {
81 | option,
82 | },
83 | });
84 |
85 | expect(wrapper.html()).toBe('Hello World ');
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/src/__tests/components/TCheckbox.integration.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { render, fireEvent } from '@testing-library/vue';
3 | import TCheckbox from '@/components/TCheckbox.vue';
4 |
5 | describe('TCheckbox.vue', () => {
6 | it('handles the v-model', async () => {
7 | const modelValue = ['A'];
8 | const { container } = render(TCheckbox, {
9 | props: {
10 | modelValue,
11 | },
12 | attrs: {
13 | name: 'checkbox-input',
14 | value: 'A',
15 | },
16 | });
17 | const { container: container2 } = render(TCheckbox, {
18 | props: {
19 | modelValue,
20 | },
21 | attrs: {
22 | name: 'checkbox-input',
23 | value: 'B',
24 | },
25 | });
26 |
27 | const input = container.querySelector('input')!;
28 | const input2 = container2.querySelector('input')!;
29 |
30 | expect(input.checked).toBe(true);
31 | expect(input2.checked).toBe(false);
32 |
33 | await fireEvent.click(input2!);
34 |
35 | expect(input.checked).toBe(true);
36 | expect(input2.checked).toBe(true);
37 |
38 | await fireEvent.click(input!);
39 |
40 | expect(input.checked).toBe(false);
41 | expect(input2.checked).toBe(true);
42 | });
43 |
44 | it('handles the v-model with not regular value types', async () => {
45 | const modelValue = [[123, 'A']];
46 | const { container } = render(TCheckbox, {
47 | props: {
48 | modelValue,
49 | },
50 | attrs: {
51 | name: 'checkbox-input',
52 | value: [123, 'A'],
53 | },
54 | });
55 | const { container: container2 } = render(TCheckbox, {
56 | props: {
57 | modelValue,
58 | },
59 | attrs: {
60 | name: 'checkbox-input',
61 | value: () => {},
62 | },
63 | });
64 | const { container: container3 } = render(TCheckbox, {
65 | props: {
66 | modelValue,
67 | },
68 | attrs: {
69 | name: 'checkbox-input',
70 | value: { A: 'B' },
71 | },
72 | });
73 |
74 | const input = container.querySelector('input')!;
75 | const input2 = container2.querySelector('input')!;
76 | const input3 = container3.querySelector('input')!;
77 |
78 | expect(input.checked).toBe(true);
79 | expect(input2.checked).toBe(false);
80 | expect(input3.checked).toBe(false);
81 |
82 | await fireEvent.click(input2!);
83 |
84 | expect(input.checked).toBe(true);
85 | expect(input2.checked).toBe(true);
86 | expect(input3.checked).toBe(false);
87 |
88 | await fireEvent.click(input3!);
89 |
90 | expect(input.checked).toBe(true);
91 | expect(input2.checked).toBe(true);
92 | expect(input3.checked).toBe(true);
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@variantjs/vue",
3 | "version": "0.0.22",
4 | "description": "Vue VariantJS: Fully configurable Vue 3 components styled with TailwindCSS",
5 | "files": [
6 | "dist/**/*",
7 | "src/**/*"
8 | ],
9 | "types": "./dist/index.d.ts",
10 | "main": "./dist/index.umd.js",
11 | "module": "./dist/index.es.js",
12 | "exports": {
13 | ".": {
14 | "import": "./dist/index.es.js",
15 | "require": "./dist/index.umd.js"
16 | }
17 | },
18 | "sideEffects": false,
19 | "keywords": [
20 | "tailwindcss",
21 | "vue",
22 | "vue-tailwind",
23 | "variantjs",
24 | "vue3"
25 | ],
26 | "author": "Alfonso Bribiesca ",
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/variantjs/vue"
30 | },
31 | "license": "MIT",
32 | "private": false,
33 | "scripts": {
34 | "build": "vue-tsc --noEmit && vite build",
35 | "serve": "vite preview",
36 | "test": "jest -t",
37 | "test:watch": "jest --watch -t",
38 | "lint": "eslint src --fix",
39 | "release": "release-it",
40 | "demo": "vite --config ./vite.demo.config.ts",
41 | "demo:build": "vite build --config ./vite.demo.config.ts",
42 | "demo:serve": "vite preview --config ./vite.demo.config.ts"
43 | },
44 | "devDependencies": {
45 | "@popperjs/core": "^2.11.0",
46 | "@rollup/plugin-typescript": "^8.3.0",
47 | "@tailwindcss/forms": "^0.4.0",
48 | "@testing-library/vue": "^6.4.2",
49 | "@types/body-scroll-lock": "^3.1.0",
50 | "@types/jest": "^27.0.3",
51 | "@typescript-eslint/eslint-plugin": "^5.8.0",
52 | "@typescript-eslint/parser": "^5.8.0",
53 | "@vitejs/plugin-vue": "^2.0.1",
54 | "@vue/cli-plugin-unit-jest": "^5.0.0-rc.1",
55 | "@vue/compiler-sfc": "^3.2.26",
56 | "@vue/test-utils": "^2.0.0-rc.6",
57 | "@vue/vue3-jest": "^27.0.0-alpha.4",
58 | "autoprefixer": "^10.4.0",
59 | "body-scroll-lock": "^4.0.0-beta.0",
60 | "eslint": "^8.5.0",
61 | "eslint-config-airbnb-typescript": "^16.1.0",
62 | "eslint-config-prettier": "^8.3.0",
63 | "eslint-plugin-import": "^2.25.3",
64 | "eslint-plugin-tree-shaking": "^1.9.2",
65 | "eslint-plugin-vue": "^8.2.0",
66 | "jest": "^27.4.5",
67 | "prettier": "^2.5.1",
68 | "release-it": "^14.11.8",
69 | "tailwindcss": "^3.0.7",
70 | "ts-jest": "^27.1.2",
71 | "ts-vue-plugin": "^0.1.3",
72 | "typescript": "^4.5.4",
73 | "vite": "^2.7.7",
74 | "vue": "^3.2.6",
75 | "vue-loader": "^16.7.0",
76 | "vue-router": "4",
77 | "vue-tsc": "^0.30.1",
78 | "vue3-jest": "^27.0.0-alpha.1"
79 | },
80 | "peerDependencies": {
81 | "@popperjs/core": "^2.11.0",
82 | "body-scroll-lock": "^4.0.0-beta.0",
83 | "vue": "^3.2.6"
84 | },
85 | "release-it": {
86 | "hooks": {
87 | "before:init": [
88 | "yarn lint",
89 | "yarn test"
90 | ],
91 | "after:bump": "yarn build"
92 | }
93 | },
94 | "dependencies": {
95 | "@variantjs/core": "^0.0.79"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/__tests/use/useMulipleableVModel.spec.ts:
--------------------------------------------------------------------------------
1 | import useMulipleableVModel from '../../use/useMulipleableVModel';
2 | import { useSetup } from './useSetup';
3 |
4 | describe('useMulipleableVModel.spec', () => {
5 | it('should return an empty array if multiple and no value', () => {
6 | const configuration = { multiple: true };
7 | useSetup(() => {
8 | const { localValue } = useMulipleableVModel({
9 | modelValue: undefined,
10 | }, 'modelValue', configuration);
11 | expect(localValue.value).toEqual([]);
12 | });
13 | });
14 |
15 | it('should return an array if multiple is an empty string', () => {
16 | const configuration = { multiple: '' };
17 |
18 | useSetup(() => {
19 | const { localValue } = useMulipleableVModel({
20 | modelValue: undefined,
21 | }, 'modelValue', configuration);
22 | expect(localValue.value).toEqual([]);
23 | });
24 | });
25 | it('should return an array if multiple is `true` string', () => {
26 | const configuration = { multiple: 'true' };
27 | useSetup(() => {
28 | const { localValue } = useMulipleableVModel({
29 | modelValue: undefined,
30 | }, 'modelValue', configuration);
31 | expect(localValue.value).toEqual([]);
32 | });
33 | });
34 |
35 | it('should return an array if multiple is `false` string', () => {
36 | const configuration = { multiple: 'false' };
37 | useSetup(() => {
38 | const { localValue } = useMulipleableVModel({
39 | modelValue: undefined,
40 | }, 'modelValue', configuration);
41 | expect(localValue.value).toEqual([]);
42 | });
43 | });
44 |
45 | it('should return undefined if multiple is `false`', () => {
46 | const configuration = { multiple: false };
47 | useSetup(() => {
48 | const { localValue } = useMulipleableVModel({
49 | modelValue: undefined,
50 | }, 'modelValue', configuration);
51 | expect(localValue.value).toBeUndefined();
52 | });
53 | });
54 |
55 | it('should return undefined if no configuration', () => {
56 | useSetup(() => {
57 | const { localValue } = useMulipleableVModel({
58 | modelValue: undefined,
59 | }, 'modelValue', undefined);
60 | expect(localValue.value).toBeUndefined();
61 | });
62 | });
63 |
64 | it('should return the default value when model changes to an undefined value', () => {
65 | const configuration = {};
66 |
67 | useSetup(() => {
68 | const { localValue } = useMulipleableVModel({
69 | modelValue: undefined,
70 | }, 'modelValue', configuration);
71 | expect(localValue.value).toBeUndefined();
72 | });
73 | });
74 |
75 | it('should return the default value when no model defined when using multiple', () => {
76 | const configuration = { multiple: true };
77 |
78 | useSetup(() => {
79 | const { localValue } = useMulipleableVModel({
80 | modelValue: undefined,
81 | }, 'modelValue', configuration);
82 | expect(localValue.value).toEqual([]);
83 | });
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/src/__tests/components/TRichSelect/RichSelectState.spec.ts:
--------------------------------------------------------------------------------
1 | import { NormalizedOption } from '@variantjs/core';
2 | import { shallowMount } from '@vue/test-utils';
3 | import { computed, ref } from 'vue';
4 | import RichSelectState from '../../../components/TRichSelect/RichSelectState.vue';
5 |
6 | describe('RichSelectState', () => {
7 | const configuration = {
8 | noResultsText: 'No results',
9 | searchingText: 'Searching...',
10 | };
11 | const options = computed(() => ([{ value: 'a', text: 'A' }]));
12 | const fetchingOptions = ref(false);
13 | const needsMoreCharsToFetch = ref(false);
14 | const needsMoreCharsMessage = ref('Needs more chars');
15 |
16 | const global = {
17 | provide: {
18 | configuration,
19 | options,
20 | fetchingOptions,
21 | needsMoreCharsToFetch,
22 | needsMoreCharsMessage,
23 | },
24 | };
25 |
26 | beforeEach(() => {
27 | fetchingOptions.value = false;
28 | needsMoreCharsToFetch.value = false;
29 | });
30 |
31 | describe('have options', () => {
32 | const wrapper = shallowMount(RichSelectState, { global });
33 |
34 | it('doesnt render anything by default', () => {
35 | expect(wrapper.text()).toBe('');
36 | });
37 |
38 | it('renders the searching text if fetchingOptions', async () => {
39 | fetchingOptions.value = true;
40 |
41 | await wrapper.vm.$nextTick();
42 |
43 | expect(wrapper.text()).toBe('Searching...');
44 | });
45 |
46 | it('renders the searching text if fetchingOptions and need more chars', async () => {
47 | fetchingOptions.value = true;
48 | needsMoreCharsToFetch.value = true;
49 |
50 | await wrapper.vm.$nextTick();
51 |
52 | expect(wrapper.text()).toBe('Searching...');
53 | });
54 |
55 | it('renders the need more chars message if need more chars', async () => {
56 | needsMoreCharsToFetch.value = true;
57 |
58 | await wrapper.vm.$nextTick();
59 |
60 | expect(wrapper.text()).toBe('Needs more chars');
61 | });
62 | });
63 |
64 | describe('doesnt have options', () => {
65 | const wrapper = shallowMount(RichSelectState, {
66 | global: {
67 | provide: {
68 | ...global.provide,
69 | options: computed(() => []),
70 | },
71 | },
72 | });
73 |
74 | it('shows the noResultsText by default', async () => {
75 | expect(wrapper.text()).toBe('No results');
76 | });
77 |
78 | it('doesnt shows the noResultsText if it need more chars to fetch ', async () => {
79 | needsMoreCharsToFetch.value = true;
80 |
81 | await wrapper.vm.$nextTick();
82 |
83 | expect(wrapper.text()).toBe('Needs more chars');
84 | });
85 |
86 | it('renders the searching text if fetchingOptions and need more chars', async () => {
87 | fetchingOptions.value = true;
88 | needsMoreCharsToFetch.value = true;
89 |
90 | await wrapper.vm.$nextTick();
91 |
92 | expect(wrapper.text()).toBe('Searching...');
93 | });
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/src/__tests/plugin.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable vue/one-component-per-file */
2 | import { createApp } from 'vue';
3 | import { variantJS } from '..';
4 | import { VariantJSConfiguration } from '../types';
5 | import { Emitter } from '../utils/emitter';
6 |
7 | describe('plugin installer', () => {
8 | it('provides the configuration', () => {
9 | const app = createApp({});
10 |
11 | const configuration: VariantJSConfiguration = {
12 | TInput: {
13 | placeholder: 'Whatever',
14 | },
15 | };
16 |
17 | app.use(variantJS, configuration);
18 |
19 | const component = app.component('ExampleComponent', {});
20 |
21 | // eslint-disable-next-line no-underscore-dangle
22 | expect(component._context.provides.configuration).toEqual(configuration);
23 | });
24 |
25 | it('handles an empty configuration', () => {
26 | const app = createApp({});
27 |
28 | app.use(variantJS);
29 |
30 | const component = app.component('ExampleComponent', {});
31 |
32 | // eslint-disable-next-line no-underscore-dangle
33 | expect(component._context.provides.configuration).toEqual({});
34 | });
35 |
36 | it('provides an emitter ', () => {
37 | const app = createApp({});
38 |
39 | app.use(variantJS);
40 |
41 | // eslint-disable-next-line no-underscore-dangle
42 | expect(app._context.provides.emitter).toBeInstanceOf(Emitter);
43 | });
44 |
45 | it('adds a $modal util as a global property', () => {
46 | const app = createApp({});
47 |
48 | app.use(variantJS);
49 |
50 | expect(typeof app.config.globalProperties.$modal).toBe('object');
51 |
52 | expect(typeof app.config.globalProperties.$modal.show).toBe('function');
53 |
54 | expect(typeof app.config.globalProperties.$modal.hide).toBe('function');
55 | });
56 |
57 | it('adds a $dialog util as a global property', () => {
58 | const app = createApp({});
59 |
60 | app.use(variantJS);
61 |
62 | expect(typeof app.config.globalProperties.$dialog).toBe('object');
63 |
64 | expect(typeof app.config.globalProperties.$dialog.show).toBe('function');
65 |
66 | expect(typeof app.config.globalProperties.$dialog.hide).toBe('function');
67 | expect(typeof app.config.globalProperties.$dialog.alert).toBe('function');
68 | expect(typeof app.config.globalProperties.$dialog.prompt).toBe('function');
69 | expect(typeof app.config.globalProperties.$dialog.confirm).toBe('function');
70 |
71 | expect(typeof app.config.globalProperties.$alert).toBe('function');
72 | expect(typeof app.config.globalProperties.$prompt).toBe('function');
73 | expect(typeof app.config.globalProperties.$confirm).toBe('function');
74 | });
75 |
76 | it('the alert, prompt and confirm methods are callable', () => {
77 | const app = createApp({});
78 |
79 | app.use(variantJS);
80 |
81 | expect(app.config.globalProperties.$confirm('Title')).toBeInstanceOf(Promise);
82 | expect(app.config.globalProperties.$alert('Title')).toBeInstanceOf(Promise);
83 | expect(app.config.globalProperties.$prompt('Title')).toBeInstanceOf(Promise);
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Import Utils
2 | import { Emitter } from './utils/emitter';
3 | import { getVariantProps, getVariantPropsWithClassesList } from './utils/getVariantProps';
4 | import { sameWidthModifier } from './utils/popper';
5 | import { svgToVueComponent } from './utils/svgToVueComponent';
6 |
7 | import plugin from './plugin';
8 |
9 | // Import Components
10 | import TInput from './components/TInput.vue';
11 | import TButton from './components/TButton.vue';
12 | import TTextarea from './components/TTextarea.vue';
13 | import TSelect from './components/TSelect.vue';
14 | import TCheckbox from './components/TCheckbox.vue';
15 | import TRadio from './components/TRadio.vue';
16 | import TAlert from './components/TAlert.vue';
17 | import TCard from './components/TCard.vue';
18 | import TDropdown from './components/TDropdown.vue';
19 | import TInputGroup from './components/TInputGroup.vue';
20 | import TRichSelect from './components/TRichSelect.vue';
21 | import TTag from './components/TTag.vue';
22 | import TToggle from './components/TToggle.vue';
23 | import TModal from './components/TModal.vue';
24 | import TDialog from './components/TDialog.vue';
25 |
26 | // Import icons
27 | import LoadingIcon from './icons/LoadingIcon.vue';
28 |
29 | // Import uses
30 | import useActivableOption from './use/useActivableOption';
31 | import useConfiguration from './use/useConfiguration';
32 | import useConfigurationWithClassesList from './use/useConfigurationWithClassesList';
33 | import useFetchsOptions from './use/useFetchsOptions';
34 | import useInjectsClassesList from './use/useInjectsClassesList';
35 | import useInjectsClassesListClass from './use/useInjectsClassesListClass';
36 | import useInjectsConfiguration from './use/useInjectsConfiguration';
37 | import useMulipleableVModel from './use/useMulipleableVModel';
38 | import useMultioptions from './use/useMultioptions';
39 | import useSelectableOption from './use/useSelectableOption';
40 | import useVModel from './use/useVModel';
41 |
42 | export * from './types';
43 |
44 | // Export components
45 | export {
46 | // Basic Form Components
47 | TInput,
48 | TButton,
49 | TTextarea,
50 | TSelect,
51 | TCheckbox,
52 | TRadio,
53 |
54 | // Form Components
55 | TInputGroup,
56 | TRichSelect,
57 |
58 | // Single tag components
59 | TTag,
60 |
61 | // Components
62 | TCard,
63 | TDropdown,
64 | TAlert,
65 | TModal,
66 | TDialog,
67 | TToggle,
68 |
69 | // Installer
70 | plugin as variantJS,
71 | };
72 |
73 | // Export icons
74 | export {
75 | LoadingIcon,
76 | };
77 |
78 | // Export utils
79 | export {
80 | Emitter,
81 | getVariantProps,
82 | getVariantPropsWithClassesList,
83 | sameWidthModifier,
84 | svgToVueComponent,
85 | };
86 |
87 | // Export uses
88 | export {
89 | useActivableOption,
90 | useConfiguration,
91 | useConfigurationWithClassesList,
92 | useFetchsOptions,
93 | useInjectsClassesList,
94 | useInjectsClassesListClass,
95 | useInjectsConfiguration,
96 | useMulipleableVModel,
97 | useMultioptions,
98 | useSelectableOption,
99 | useVModel,
100 | };
101 |
--------------------------------------------------------------------------------
/src/components/TButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
110 |
--------------------------------------------------------------------------------
/src/__tests/components/TInput.integration.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { render, fireEvent } from '@testing-library/vue';
3 | import TInput from '@/components/TInput.vue';
4 |
5 | describe('TInput.vue', () => {
6 | it('handles the v-model', async () => {
7 | const { container, getByDisplayValue } = render(TInput);
8 |
9 | const input = container.querySelector('input')!;
10 |
11 | await fireEvent.update(input!, 'Alfonso');
12 |
13 | getByDisplayValue('Alfonso');
14 | });
15 |
16 | it('contains the class + classes + fixedClasses', async () => {
17 | const { container } = render(TInput, {
18 | props: {
19 | fixedClasses: 'text-red-500',
20 | classes: 'border-red-500',
21 | class: 'font-semibold',
22 | },
23 | });
24 |
25 | const input = container.querySelector('.text-red-500.border-red-500.font-semibold');
26 | expect(input).not.toBeNull();
27 | });
28 |
29 | it('adds the html attributes', async () => {
30 | const { getByPlaceholderText, getByRole, getByTitle } = render(TInput, {
31 | props: {
32 | placeholder: 'Write something',
33 | role: 'text-field',
34 | title: 'my title',
35 | },
36 | });
37 |
38 | getByPlaceholderText('Write something');
39 |
40 | getByRole('text-field');
41 |
42 | getByTitle('my title');
43 | });
44 |
45 | it('adds the classes on the variant', async () => {
46 | const { container } = render(TInput, {
47 | props: {
48 | variants: {
49 | error: {
50 | classes: 'text-red-500',
51 | },
52 | },
53 | variant: 'error',
54 | classes: 'text-blue-500',
55 | },
56 | });
57 |
58 | let input = container.querySelector('.text-red-500');
59 | expect(input).not.toBeNull();
60 |
61 | input = container.querySelector('.text-blue-500');
62 | expect(input).toBeNull();
63 | });
64 |
65 | it('keeps the fixedClasses when using a variant', async () => {
66 | const { container } = render(TInput, {
67 | props: {
68 | variants: {
69 | error: {
70 | classes: 'text-red-500',
71 | },
72 | },
73 | variant: 'error',
74 | fixedClasses: 'text-blue-500',
75 | },
76 | });
77 |
78 | let input = container.querySelector('.text-red-500');
79 | expect(input).not.toBeNull();
80 |
81 | input = container.querySelector('.text-blue-500');
82 | expect(input).not.toBeNull();
83 | });
84 |
85 | it('overrides the fixedClasses when using a variant', async () => {
86 | const { container } = render(TInput, {
87 | props: {
88 | variants: {
89 | error: {
90 | fixedClasses: 'text-red-500',
91 | },
92 | },
93 | variant: 'error',
94 | fixedClasses: 'text-blue-500',
95 | },
96 | });
97 |
98 | let input = container.querySelector('.text-red-500');
99 | expect(input).not.toBeNull();
100 |
101 | input = container.querySelector('.text-blue-500');
102 | expect(input).toBeNull();
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/src/__tests/components/TTextarea.integration.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { render, fireEvent } from '@testing-library/vue';
3 | import TTextarea from '@/components/TTextarea.vue';
4 |
5 | describe('TTextarea.vue', () => {
6 | it('handles the v-model', async () => {
7 | const { container, getByDisplayValue } = render(TTextarea);
8 |
9 | const input = container.querySelector('textarea')!;
10 |
11 | await fireEvent.update(input!, 'Alfonso');
12 |
13 | getByDisplayValue('Alfonso');
14 | });
15 |
16 | it('contains the class + classes + fixedClasses', async () => {
17 | const { container } = render(TTextarea, {
18 | props: {
19 | fixedClasses: 'text-red-500',
20 | classes: 'border-red-500',
21 | class: 'font-semibold',
22 | },
23 | });
24 |
25 | const input = container.querySelector('.text-red-500.border-red-500.font-semibold');
26 | expect(input).not.toBeNull();
27 | });
28 |
29 | it('adds the html attributes', async () => {
30 | const { getByPlaceholderText, getByRole, getByTitle } = render(TTextarea, {
31 | props: {
32 | placeholder: 'Write something',
33 | role: 'text-field',
34 | title: 'my title',
35 | },
36 | });
37 |
38 | getByPlaceholderText('Write something');
39 |
40 | getByRole('text-field');
41 |
42 | getByTitle('my title');
43 | });
44 |
45 | it('adds the classes on the variant', async () => {
46 | const { container } = render(TTextarea, {
47 | props: {
48 | variants: {
49 | error: {
50 | classes: 'text-red-500',
51 | },
52 | },
53 | variant: 'error',
54 | classes: 'text-blue-500',
55 | },
56 | });
57 |
58 | let input = container.querySelector('.text-red-500');
59 | expect(input).not.toBeNull();
60 |
61 | input = container.querySelector('.text-blue-500');
62 | expect(input).toBeNull();
63 | });
64 |
65 | it('keeps the fixedClasses when using a variant', async () => {
66 | const { container } = render(TTextarea, {
67 | props: {
68 | variants: {
69 | error: {
70 | classes: 'text-red-500',
71 | },
72 | },
73 | variant: 'error',
74 | fixedClasses: 'text-blue-500',
75 | },
76 | });
77 |
78 | let input = container.querySelector('.text-red-500');
79 | expect(input).not.toBeNull();
80 |
81 | input = container.querySelector('.text-blue-500');
82 | expect(input).not.toBeNull();
83 | });
84 |
85 | it('overrides the fixedClasses when using a variant', async () => {
86 | const { container } = render(TTextarea, {
87 | props: {
88 | variants: {
89 | error: {
90 | fixedClasses: 'text-red-500',
91 | },
92 | },
93 | variant: 'error',
94 | fixedClasses: 'text-blue-500',
95 | },
96 | });
97 |
98 | let input = container.querySelector('.text-red-500');
99 | expect(input).not.toBeNull();
100 |
101 | input = container.querySelector('.text-blue-500');
102 | expect(input).toBeNull();
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/src/__tests/use/useMultioptions.spec.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 | import useMultioptions from '../../use/useMultioptions';
3 | import { useSetup } from './useSetup';
4 |
5 | describe('useMultioptions', () => {
6 | const options = ref(['A', 'B']);
7 | const textAttribute = ref(undefined);
8 | const valueAttribute = ref(undefined);
9 | const normalize = ref(true);
10 | it('returns normalized options', () => {
11 | useSetup(() => {
12 | const { normalizedOptions } = useMultioptions(
13 | options,
14 | textAttribute,
15 | valueAttribute,
16 | normalize,
17 | );
18 |
19 | expect(normalizedOptions.value).toEqual([
20 | { raw: 'A', text: 'A', value: 'A' },
21 | { raw: 'B', text: 'B', value: 'B' },
22 | ]);
23 | });
24 | });
25 |
26 | it('returns flattened options', () => {
27 | useSetup(() => {
28 | const { flattenedOptions } = useMultioptions(
29 | ref([
30 | { text: 'A', value: 'A' },
31 | {
32 | text: 'B', value: 'B', children: ['C'],
33 | },
34 | ]),
35 | textAttribute,
36 | valueAttribute,
37 | normalize,
38 | );
39 |
40 | expect(flattenedOptions.value).toEqual(
41 | [
42 | { value: 'A', text: 'A', raw: { text: 'A', value: 'A' } },
43 | { value: 'C', text: 'C', raw: 'C' },
44 | ],
45 | );
46 | });
47 | });
48 |
49 | it('handles undefined options', () => {
50 | useSetup(() => {
51 | const { normalizedOptions } = useMultioptions(
52 | ref(undefined),
53 | textAttribute,
54 | valueAttribute,
55 | normalize,
56 | );
57 |
58 | expect(normalizedOptions.value).toEqual([]);
59 | });
60 | });
61 |
62 | it('accepts a custom `textAttribute`', () => {
63 | useSetup(() => {
64 | const { normalizedOptions } = useMultioptions(
65 | ref([
66 | { label: 'Letter A', value: 'A' },
67 | { label: 'Letter B', value: 'B' },
68 | ]),
69 | ref('label'),
70 | valueAttribute,
71 | normalize,
72 | );
73 |
74 | expect(normalizedOptions.value).toEqual([
75 | { raw: { label: 'Letter A', value: 'A' }, text: 'Letter A', value: 'A' },
76 | { raw: { label: 'Letter B', value: 'B' }, text: 'Letter B', value: 'B' },
77 | ]);
78 | });
79 | });
80 |
81 | it('accepts a custom `valueAttribute`', () => {
82 | useSetup(() => {
83 | const { normalizedOptions } = useMultioptions(
84 | ref([
85 | { text: 'A', identifier: 'a' },
86 | { text: 'B', identifier: 'b' },
87 | ]),
88 | textAttribute,
89 | ref('identifier'),
90 | normalize,
91 | );
92 |
93 | expect(normalizedOptions.value).toEqual([
94 | { raw: { text: 'A', identifier: 'a' }, text: 'A', value: 'a' },
95 | { raw: { text: 'B', identifier: 'b' }, text: 'B', value: 'b' },
96 | ]);
97 | });
98 | });
99 |
100 | it('doesnt normalize the options if normalize is `false`', () => {
101 | useSetup(() => {
102 | const { normalizedOptions } = useMultioptions(
103 | options,
104 | textAttribute,
105 | valueAttribute,
106 | ref(false),
107 | );
108 |
109 | expect(normalizedOptions.value).toEqual(options.value);
110 | });
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DialogHideFn, DialogProgramaticallyShowFn, DialogResponse, DialogShowFn, DialogType, ModalHideFn, ModalShowFn,
3 | } from '@variantjs/core';
4 | import { App } from 'vue';
5 | import { VariantJSConfiguration } from './types';
6 | import { TDialogOptions } from './types/components/t-dialog';
7 | import createDialogProgramatically from './utils/createDialogProgramatically';
8 | import { Emitter } from './utils/emitter';
9 |
10 | const plugin = {
11 | install: (app: App, configuration: VariantJSConfiguration = {}): void => {
12 | const emitter = new Emitter();
13 |
14 | // @TODO: ensure this variable is exposed for https://vuetelescope.com/
15 | // eslint-disable-next-line no-param-reassign
16 | app.config.globalProperties.$variantJS = true;
17 |
18 | // eslint-disable-next-line no-param-reassign
19 | app.config.globalProperties.$modal = {
20 | show(name: string, params?: { [k: string]: string }) {
21 | emitter.emit('modal:show', name, params);
22 | },
23 | hide(name: string) {
24 | emitter.emit('modal:hide', name);
25 | },
26 | };
27 |
28 | const alert: DialogProgramaticallyShowFn = (titleOrDialogOptions: TDialogOptions | string, text?: string, icon?: string) : Promise => createDialogProgramatically(configuration, DialogType.Alert, titleOrDialogOptions, text, icon);
29 |
30 | const prompt: DialogProgramaticallyShowFn = (titleOrDialogOptions: TDialogOptions | string, text?: string, icon?: string) : Promise => createDialogProgramatically(configuration, DialogType.Prompt, titleOrDialogOptions, text, icon);
31 |
32 | const confirm: DialogProgramaticallyShowFn = (titleOrDialogOptions: TDialogOptions | string, text?: string, icon?: string) : Promise => createDialogProgramatically(configuration, DialogType.Confirm, titleOrDialogOptions, text, icon);
33 |
34 | // eslint-disable-next-line no-param-reassign
35 | app.config.globalProperties.$dialog = {
36 | show(name: string): Promise {
37 | const promise = new Promise((resolve, reject) => {
38 | emitter.emit('dialog:show', name, resolve, reject);
39 | });
40 |
41 | return promise as Promise;
42 | },
43 | hide(name: string) {
44 | emitter.emit('dialog:hide', name);
45 | },
46 | alert,
47 | confirm,
48 | prompt,
49 | };
50 |
51 | // eslint-disable-next-line no-param-reassign
52 | app.config.globalProperties.$alert = alert;
53 | // eslint-disable-next-line no-param-reassign
54 | app.config.globalProperties.$confirm = confirm;
55 | // eslint-disable-next-line no-param-reassign
56 | app.config.globalProperties.$prompt = prompt;
57 |
58 | app.provide('configuration', configuration);
59 |
60 | app.provide('emitter', emitter);
61 | },
62 | };
63 |
64 | declare module '@vue/runtime-core' {
65 | interface ComponentCustomProperties {
66 | $variantJS: boolean;
67 | $modal: {
68 | show: ModalShowFn;
69 | hide: ModalHideFn;
70 | },
71 | $dialog: {
72 | show: DialogShowFn;
73 | hide: DialogHideFn;
74 | alert: DialogProgramaticallyShowFn;
75 | confirm: DialogProgramaticallyShowFn;
76 | prompt: DialogProgramaticallyShowFn;
77 | },
78 | $alert: DialogProgramaticallyShowFn;
79 | $confirm: DialogProgramaticallyShowFn;
80 | $prompt: DialogProgramaticallyShowFn;
81 | }
82 | }
83 |
84 | export default plugin;
85 |
--------------------------------------------------------------------------------
/src/use/useConfiguration.ts:
--------------------------------------------------------------------------------
1 | import {
2 | computed, inject, camelize, getCurrentInstance, ComponentInternalInstance, ComputedRef, watch, reactive,
3 | } from 'vue';
4 | import {
5 | Data, get, isEqual, isPrimitive, parseVariant, pick,
6 | } from '@variantjs/core';
7 | import { VariantJSConfiguration } from '../types';
8 |
9 | export const extractDefinedProps = (vm: ComponentInternalInstance): string[] => {
10 | const validProps = Object.keys(vm.props);
11 |
12 | const definedProps = Object.keys(vm.vnode.props || {})
13 | .map((propName) => camelize(propName))
14 | .filter((propName) => validProps.includes(propName) && propName !== 'modelValue');
15 |
16 | return definedProps;
17 | };
18 |
19 | export function useAttributes(configuration: ComponentOptions): Data {
20 | const vm = getCurrentInstance()!;
21 |
22 | const computedAttributes: ComputedRef = computed(():Data => {
23 | const availableProps = Object.keys(vm.props);
24 |
25 | return pick(configuration, (value, key) => isPrimitive(value) && !availableProps.includes(String(key)));
26 | });
27 |
28 | const attributes = reactive(computedAttributes.value);
29 |
30 | watch(computedAttributes, (newValue) => {
31 | Object.keys(newValue).forEach((key) => {
32 | if (!isEqual(attributes[key], newValue[key])) {
33 | attributes[key] = newValue[key];
34 | }
35 | });
36 | });
37 |
38 | return attributes;
39 | }
40 |
41 | export function useConfigurationParts(): {
42 | componentGlobalConfiguration?: ComponentOptions
43 | propsValues: ComputedRef
44 | } {
45 | const vm = getCurrentInstance()!;
46 |
47 | const variantGlobalConfiguration = inject('configuration', {});
48 |
49 | const componentGlobalConfiguration = get(variantGlobalConfiguration, vm?.type.name as keyof VariantJSConfiguration, {});
50 |
51 | const propsValues = computed(() => {
52 | const values: Data = {};
53 |
54 | extractDefinedProps(vm).forEach((attributeName) => {
55 | const normalizedAttribute = camelize(attributeName);
56 | values[normalizedAttribute] = vm.props[normalizedAttribute];
57 | });
58 |
59 | return values;
60 | });
61 |
62 | return {
63 | componentGlobalConfiguration,
64 | propsValues: propsValues as ComputedRef,
65 | };
66 | }
67 |
68 | export default function useConfiguration(defaultConfiguration: ComponentOptions): {
69 | configuration: ComponentOptions,
70 | attributes: Data,
71 | } {
72 | const vm = getCurrentInstance()!;
73 |
74 | const { propsValues, componentGlobalConfiguration } = useConfigurationParts();
75 |
76 | const computedConfiguration = computed(() => {
77 | const props = { ...vm.props };
78 | delete props.modelValue;
79 | return {
80 | ...props,
81 | ...parseVariant(
82 | propsValues.value,
83 | componentGlobalConfiguration,
84 | defaultConfiguration,
85 | ),
86 | };
87 | });
88 |
89 | const configuration = reactive(computedConfiguration.value);
90 |
91 | watch(computedConfiguration, (newValue) => {
92 | Object.keys(newValue).forEach((key) => {
93 | configuration[key] = newValue[key];
94 | });
95 | });
96 |
97 | const attributes = useAttributes(configuration);
98 |
99 | return {
100 | configuration: configuration as ComponentOptions,
101 | attributes,
102 | };
103 | }
104 |
--------------------------------------------------------------------------------
/src/__tests/components/TRichSelect/RichSelectSearchInput.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { shallowMount } from '@vue/test-utils';
3 | import { ref } from 'vue';
4 | import RichSelectSearchInput from '../../../components/TRichSelect/RichSelectSearchInput.vue';
5 |
6 | describe('RichSelectSearchInput', () => {
7 | const keydownDownHandler = jest.fn();
8 | const keydownUpHandler = jest.fn();
9 | const keydownEnterHandler = jest.fn();
10 | const keydownEscHandler = jest.fn();
11 |
12 | const global = {
13 | provide: {
14 | shown: ref(true),
15 | searchQuery: ref(''),
16 | keydownDownHandler,
17 | keydownUpHandler,
18 | keydownEnterHandler,
19 | keydownEscHandler,
20 | },
21 | };
22 |
23 | let wrapper: any;
24 | let search: HTMLInputElement;
25 |
26 | beforeEach(() => {
27 | wrapper = shallowMount(RichSelectSearchInput, { global });
28 |
29 | search = wrapper.vm.$refs.search as HTMLInputElement;
30 | });
31 |
32 | it('renders the component without errors', () => {
33 | expect(wrapper.vm.$el.tagName).toBe('DIV');
34 | });
35 |
36 | it('calls the keydownDownHandler when down key pressed', () => {
37 | const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
38 |
39 | search.dispatchEvent(event);
40 |
41 | expect(keydownDownHandler).toHaveBeenCalled();
42 | });
43 |
44 | it('calls the keydownUpHandler when down key pressed', () => {
45 | const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
46 |
47 | search.dispatchEvent(event);
48 |
49 | expect(keydownUpHandler).toHaveBeenCalled();
50 | });
51 |
52 | it('calls the keydownEnterHandler when down key pressed', () => {
53 | const event = new KeyboardEvent('keydown', { key: 'enter' });
54 |
55 | search.dispatchEvent(event);
56 |
57 | expect(keydownEnterHandler).toHaveBeenCalled();
58 | });
59 |
60 | it('calls the keydownEscHandler when down key pressed', () => {
61 | const event = new KeyboardEvent('keydown', { key: 'esc' });
62 |
63 | search.dispatchEvent(event);
64 |
65 | expect(keydownEscHandler).toHaveBeenCalled();
66 | });
67 |
68 | it('focus the search field after is shown', async () => {
69 | const shown = ref(false);
70 |
71 | wrapper = shallowMount(RichSelectSearchInput, {
72 | global: {
73 | provide: {
74 | ...global.provide,
75 | shown,
76 | },
77 | },
78 | });
79 |
80 | const searchInput = wrapper.vm.$el.querySelector('input');
81 |
82 | const focusSpy = jest.spyOn(searchInput, 'focus');
83 |
84 | shown.value = true;
85 |
86 | await wrapper.vm.$nextTick();
87 |
88 | await wrapper.vm.$nextTick();
89 |
90 | expect(focusSpy).toHaveBeenCalled();
91 |
92 | focusSpy.mockRestore();
93 | });
94 |
95 | it('doesnt focus the search field after is hidden', async () => {
96 | const shown = ref(true);
97 |
98 | wrapper = shallowMount(RichSelectSearchInput, {
99 | global: {
100 | provide: {
101 | ...global.provide,
102 | shown,
103 | },
104 | },
105 | });
106 |
107 | const searchInput = wrapper.vm.$el.querySelector('input');
108 |
109 | const focusSpy = jest.spyOn(searchInput, 'focus');
110 |
111 | shown.value = false;
112 |
113 | await wrapper.vm.$nextTick();
114 |
115 | await wrapper.vm.$nextTick();
116 |
117 | expect(focusSpy).not.toHaveBeenCalled();
118 |
119 | focusSpy.mockRestore();
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/src/__tests/components/TRadio.integration.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import { render, fireEvent } from '@testing-library/vue';
3 | import TRadio from '@/components/TRadio.vue';
4 |
5 | describe('TRadio.vue', () => {
6 | it('handles the v-model', async () => {
7 | const modelValue = 'A';
8 | const { container } = render(TRadio, {
9 | props: {
10 | modelValue,
11 | },
12 | attrs: {
13 | name: 'radio-input',
14 | value: 'A',
15 | },
16 | });
17 | const { container: container2 } = render(TRadio, {
18 | props: {
19 | modelValue,
20 | },
21 | attrs: {
22 | name: 'radio-input',
23 | value: 'B',
24 | },
25 | });
26 |
27 | const input = container.querySelector('input')!;
28 | const input2 = container2.querySelector('input')!;
29 |
30 | expect(input.checked).toBe(true);
31 | expect(input2.checked).toBe(false);
32 |
33 | await fireEvent.update(input2!, 'B');
34 |
35 | expect(input.checked).toBe(false);
36 | expect(input2.checked).toBe(true);
37 | });
38 |
39 | it('handles the v-model with not regular value types', async () => {
40 | const modelValue = [123, 'A'];
41 | const { container } = render(TRadio, {
42 | props: {
43 | modelValue,
44 | },
45 | attrs: {
46 | name: 'radio-input',
47 | value: modelValue,
48 | },
49 | });
50 | const { container: container2 } = render(TRadio, {
51 | props: {
52 | modelValue,
53 | },
54 | attrs: {
55 | name: 'radio-input',
56 | value: () => {},
57 | },
58 | });
59 | const { container: container3 } = render(TRadio, {
60 | props: {
61 | modelValue,
62 | },
63 | attrs: {
64 | name: 'radio-input',
65 | value: { A: 'B' },
66 | },
67 | });
68 |
69 | const input = container.querySelector('input')!;
70 | const input2 = container2.querySelector('input')!;
71 | const input3 = container3.querySelector('input')!;
72 |
73 | expect(input.checked).toBe(true);
74 | expect(input2.checked).toBe(false);
75 | expect(input3.checked).toBe(false);
76 |
77 | await fireEvent.update(input2!);
78 |
79 | expect(input.checked).toBe(false);
80 | expect(input2.checked).toBe(true);
81 | expect(input3.checked).toBe(false);
82 |
83 | await fireEvent.update(input3!);
84 |
85 | expect(input.checked).toBe(false);
86 | expect(input2.checked).toBe(false);
87 | expect(input3.checked).toBe(true);
88 | });
89 |
90 | it('handles the v-model independently if radio name is different', async () => {
91 | const modelValue = 'A';
92 | const { container } = render(TRadio, {
93 | props: {
94 | modelValue,
95 | },
96 | attrs: {
97 | name: 'radio-input',
98 | value: 'A',
99 | },
100 | });
101 | const { container: container2 } = render(TRadio, {
102 | props: {
103 | modelValue,
104 | },
105 | attrs: {
106 | name: 'radio-input-b',
107 | value: 'B',
108 | },
109 | });
110 |
111 | const input = container.querySelector('input')!;
112 | const input2 = container2.querySelector('input')!;
113 |
114 | expect(input.checked).toBe(true);
115 | expect(input2.checked).toBe(false);
116 |
117 | await fireEvent.update(input2!, 'B');
118 |
119 | expect(input.checked).toBe(true);
120 | expect(input2.checked).toBe(true);
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/src/components/TInputGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
19 |
20 | {{ configuration[element.name === 'default' ? 'body' : element.name] }}
21 |
22 |
23 |
24 |
25 |
26 |
108 |
--------------------------------------------------------------------------------
/src/use/useActivableOption.ts:
--------------------------------------------------------------------------------
1 | import { isEqual, NormalizedOption, normalizedOptionIsDisabled } from '@variantjs/core';
2 | import {
3 | computed, ComputedRef, Ref, ref, watch,
4 | } from 'vue';
5 |
6 | export default function useActivableOption(
7 | options: ComputedRef,
8 | localValue: Ref,
9 | ): {
10 | activeOption: Ref,
11 | initActiveOption: () => void,
12 | optionIsActive: (option: NormalizedOption) => boolean,
13 | setActiveOption: (option: NormalizedOption) => void,
14 | setNextOptionActive: () => void,
15 | setPrevOptionActive: () => void,
16 | } {
17 | const getActiveOption = (): NormalizedOption | null => {
18 | const selectedOption = options.value.find((option: NormalizedOption) => isEqual(option.value, localValue.value) && !normalizedOptionIsDisabled(option));
19 |
20 | if (selectedOption !== undefined) {
21 | return selectedOption;
22 | }
23 |
24 | if (options.value.length > 0) {
25 | return options.value.find((option) => !normalizedOptionIsDisabled(option)) || null;
26 | }
27 |
28 | return null;
29 | };
30 |
31 | const activeOption = ref(getActiveOption());
32 |
33 | const activeOptionIndex = computed((): number => {
34 | if (activeOption.value === null) {
35 | return 0;
36 | }
37 |
38 | const index = options.value.findIndex((option) => isEqual(option.value, (activeOption.value as NormalizedOption).value));
39 |
40 | return index < 0 ? 0 : index;
41 | });
42 |
43 | const optionIsActive = (option: NormalizedOption): boolean => (activeOption.value === null ? false : isEqual(activeOption.value.value, option.value));
44 |
45 | const setActiveOption = (option: NormalizedOption): void => {
46 | activeOption.value = option;
47 | };
48 |
49 | const setNextOptionActive = (): void => {
50 | let newActiveOption: NormalizedOption | undefined;
51 | let nextIndex = activeOptionIndex.value + 1;
52 |
53 | while (nextIndex < options.value.length && newActiveOption === undefined) {
54 | const option = options.value[nextIndex];
55 | const optionIsDisabled = normalizedOptionIsDisabled(option);
56 | if (!optionIsDisabled) {
57 | newActiveOption = option;
58 | }
59 | nextIndex += 1;
60 | }
61 |
62 | if (newActiveOption) {
63 | setActiveOption(newActiveOption);
64 | }
65 | };
66 |
67 | const setPrevOptionActive = (): void => {
68 | let newActiveOption: NormalizedOption | undefined;
69 | let nextIndex = activeOptionIndex.value - 1;
70 |
71 | while (nextIndex >= 0 && newActiveOption === undefined) {
72 | const option = options.value[nextIndex];
73 | const optionIsDisabled = normalizedOptionIsDisabled(option);
74 | if (!optionIsDisabled) {
75 | newActiveOption = option;
76 | }
77 | nextIndex -= 1;
78 | }
79 |
80 | if (newActiveOption) {
81 | setActiveOption(newActiveOption);
82 | }
83 | };
84 |
85 | const initActiveOption = (): void => {
86 | activeOption.value = getActiveOption();
87 | };
88 |
89 | watch(options, (newOptions: NormalizedOption[], oldOptions: NormalizedOption[]) => {
90 | const firstNewOption = newOptions.find((o) => !oldOptions.includes(o) && !normalizedOptionIsDisabled(o));
91 |
92 | if (firstNewOption) {
93 | setActiveOption(firstNewOption);
94 | } else {
95 | initActiveOption();
96 | }
97 | });
98 |
99 | return {
100 | activeOption,
101 | initActiveOption,
102 | optionIsActive,
103 | setActiveOption,
104 | setNextOptionActive,
105 | setPrevOptionActive,
106 | };
107 | }
108 |
--------------------------------------------------------------------------------
/src/types/variantCore.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CSSRawClassesList,
3 | CSSClass, Variants, VariantsWithClassesList, WithVariantProps, WithVariantPropsAndClassesList, Data,
4 | } from '@variantjs/core';
5 |
6 | import { ComponentPropsOptions, PropType } from 'vue';
7 | import { TButtonOptions } from './components/t-button';
8 | import { TCardOptions } from './components/t-card';
9 | import { TCheckboxOptions } from './components/t-checkbox';
10 | import { TInputOptions } from './components/t-input';
11 | import { TInputGroupOptions } from './components/t-input-group';
12 | import { TRadioOptions } from './components/t-radio';
13 | import { TSelectOptions } from './components/t-select';
14 | import { TTagOptions } from './components/t-tag';
15 | import { TTextareaOptions } from './components/t-textarea';
16 |
17 | type VariantJSProps = {
18 | classes?: CSSClass;
19 | fixedClasses?: CSSClass;
20 | variants?: Variants;
21 | variant?: string;
22 | class?: string;
23 | }, PropsOptions extends Readonly = {
24 | classes: {
25 | type: PropType;
26 | default: undefined;
27 | },
28 | fixedClasses: {
29 | type: PropType;
30 | default: undefined;
31 | },
32 | variants: {
33 | type: PropType>;
34 | default: undefined;
35 | },
36 | variant: {
37 | type:PropType;
38 | default: undefined;
39 | },
40 | }> = PropsOptions & {
41 | classes: {
42 | type: PropType;
43 | default: undefined;
44 | },
45 | fixedClasses: {
46 | type: PropType;
47 | default: undefined;
48 | },
49 | variants: {
50 | type: PropType>;
51 | default: undefined;
52 | },
53 | variant: {
54 | type:PropType;
55 | default: undefined;
56 | },
57 | };
58 |
59 | type VariantJSWithClassesListProps<
60 | ClassesKeys extends string,
61 | ComponentOptions extends WithVariantPropsAndClassesList = WithVariantPropsAndClassesList,
62 | PropsOptions extends Readonly = {
63 | classes: {
64 | type: PropType>;
65 | default: undefined;
66 | },
67 | fixedClasses: {
68 | type: PropType>;
69 | default: undefined;
70 | },
71 | variants: {
72 | type: PropType>;
73 | default: undefined;
74 | },
75 | variant: {
76 | type:PropType;
77 | default: undefined;
78 | },
79 | }> = PropsOptions & {
80 | classes: {
81 | type: PropType>;
82 | default: undefined;
83 | },
84 | fixedClasses: {
85 | type: PropType>;
86 | default: undefined;
87 | },
88 | variants: {
89 | type: PropType>;
90 | default: undefined;
91 | },
92 | variant: {
93 | type:PropType;
94 | default: undefined;
95 | },
96 | };
97 |
98 | type VariantJSConfiguration = {
99 | TInput?: TInputOptions
100 | TSelect?: TSelectOptions
101 | TRadio?: TRadioOptions
102 | TCheckbox?: TCheckboxOptions
103 | TButton?: TButtonOptions
104 | TTextarea?: TTextareaOptions
105 | TTag?: TTagOptions
106 | TCard?: TCardOptions
107 | TInputGroup?: TInputGroupOptions,
108 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
109 | [key: string]: any
110 | };
111 |
112 | export { VariantJSConfiguration, VariantJSProps, VariantJSWithClassesListProps };
113 |
--------------------------------------------------------------------------------
/src/__tests/utils/createDialogProgramatically.spec.ts:
--------------------------------------------------------------------------------
1 | import { DialogIcon, DialogType } from '@variantjs/core';
2 | import { VariantJSConfiguration } from '../..';
3 | import createDialogProgramatically from '../../utils/createDialogProgramatically';
4 | import TDialog from '@/components/TDialog.vue';
5 |
6 | describe('createDialogProgramatically', () => {
7 | const configuration: VariantJSConfiguration = {};
8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
9 | let originalMounted: any;
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | let component: any;
12 |
13 | beforeEach(() => {
14 | originalMounted = TDialog.mounted;
15 |
16 | // eslint-disable-next-line func-names
17 | TDialog.mounted = function () {
18 | component = this;
19 | };
20 | });
21 |
22 | afterEach(() => {
23 | TDialog.mounted = originalMounted;
24 |
25 | component = undefined;
26 | });
27 |
28 | it('creates a dialog with a title, text and icon', () => {
29 | const promise = createDialogProgramatically(
30 | configuration,
31 | DialogType.Alert,
32 | 'The title',
33 | 'The text',
34 | DialogIcon.Error,
35 | );
36 |
37 | expect(promise).toBeInstanceOf(Promise);
38 |
39 | expect(component.title).toEqual('The title');
40 | expect(component.text).toEqual('The text');
41 | expect(component.icon).toEqual(DialogIcon.Error);
42 | expect(component.type).toEqual(DialogType.Alert);
43 | });
44 |
45 | it('creates a dialog with configuration', async () => {
46 | const promise = createDialogProgramatically(
47 | configuration,
48 | DialogType.Alert,
49 | {
50 | title: 'The title',
51 | text: 'The text',
52 | icon: DialogIcon.Error,
53 | },
54 | );
55 |
56 | expect(promise).toBeInstanceOf(Promise);
57 |
58 | expect(component.title).toEqual('The title');
59 | expect(component.text).toEqual('The text');
60 | expect(component.icon).toEqual(DialogIcon.Error);
61 | expect(component.type).toEqual(DialogType.Alert);
62 | });
63 |
64 | it('unmounts the dialog when promise resolved', async () => {
65 | const unmountMock = jest.fn();
66 | TDialog.unmounted = unmountMock;
67 |
68 | createDialogProgramatically(
69 | configuration,
70 | DialogType.Alert,
71 | {
72 | title: 'The title',
73 | text: 'The text',
74 | icon: DialogIcon.Error,
75 | focusOnOpen: false,
76 | disableBodyScroll: false,
77 | },
78 | );
79 |
80 | component.onUnmounted = unmountMock;
81 |
82 | component.onBeforeHide({
83 | cancel: () => null,
84 | reason: 'other',
85 | });
86 |
87 | component.onHidden();
88 |
89 | await component.$nextTick();
90 |
91 | expect(unmountMock).toHaveBeenCalled();
92 | });
93 |
94 | it('unmounts the dialog when promise rejected', async () => {
95 | const unmountMock = jest.fn();
96 | TDialog.unmounted = unmountMock;
97 |
98 | const promise = createDialogProgramatically(
99 | configuration,
100 | DialogType.Prompt,
101 | {
102 | title: 'The title',
103 | text: 'The text',
104 | icon: DialogIcon.Error,
105 | focusOnOpen: false,
106 | disableBodyScroll: false,
107 | },
108 | );
109 |
110 | promise.catch(() => {
111 | expect(true).toBe(true);
112 | });
113 |
114 | component.onUnmounted = unmountMock;
115 |
116 | component.onBeforeHide({
117 | cancel: () => null,
118 | reason: 'cancel',
119 | });
120 |
121 | component.onHidden();
122 |
123 | await component.$nextTick();
124 | await component.$nextTick();
125 |
126 | expect(unmountMock).toHaveBeenCalled();
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/src/development/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | List of components
5 |
6 |
7 |
8 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Aliquam id iusto quas quaerat quasi cum alias consequuntur perspiciatis, quam ea. Quibusdam incidunt ex vel illum ab quaerat, sed tempora beatae!
9 |
10 |
11 |
16 |
20 |
21 |
22 |
23 | Check me
24 |
25 |
26 |
27 |
28 |
34 |
35 | Select this
36 |
37 |
38 |
43 |
44 | And select this
45 |
46 |
47 |
{{ checkboxValue }}
48 |
49 |
50 |
51 |
52 |
58 |
59 | Select this ({{ radioValue }})
60 |
61 |
62 |
67 |
68 | Or select this ({{ radioValue }})
69 |
70 |
71 |
72 |
73 | My button
74 |
75 | TSubmit (Custom component)
76 |
77 |
78 |
79 |
80 | Footer content
81 |
82 |
83 |
84 |
85 |
134 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectTriggerTagsTag.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 | {{ option.text }}
21 |
22 |
23 |
32 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
131 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectOptionsList.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
13 |
14 |
18 |
19 |
20 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
40 |
41 |
42 |
43 |
130 |
--------------------------------------------------------------------------------
/src/development/Check.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
13 |
14 | Value: {{ selected }}
15 |
16 |
20 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
36 |
42 |
48 |
49 | Value: {{ accepted }}
50 |
51 |
52 |
56 |
57 |
58 |
62 |
63 |
64 |
68 |
69 |
75 |
81 |
87 |
88 |
Value {{ multiple }}
89 |
90 |
91 |
92 |
97 |
102 |
107 |
108 |
Value {{ multiple }}
109 |
110 |
129 |
130 |
131 |
135 |
139 |
140 |
144 |
147 |
148 |
149 |
150 |
151 |
177 |
--------------------------------------------------------------------------------
/src/development/Options.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Options components v-model sync
5 |
9 |
10 |
15 |
16 |
21 |
22 |
29 |
30 |
34 |
38 |
39 |
40 |
47 | {{ movie?.Title }}
48 |
49 |
56 | {{ movie?.Year }}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
69 |
70 |
71 |
72 | Clear value
73 |
74 |
75 |
76 |
77 |
138 |
--------------------------------------------------------------------------------
/src/components/TRichSelect/RichSelectTrigger.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
47 |
52 | {{ label }}
53 |
54 |
55 |
56 |
60 |
64 |
65 |
66 |
67 |
149 |
--------------------------------------------------------------------------------
/src/use/useSelectableOption.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import {
3 | addToArray, isEqual, NormalizedOption, substractFromArray,
4 | } from '@variantjs/core';
5 | import {
6 | computed, ComputedRef, Ref, ref, watch,
7 | } from 'vue';
8 |
9 | type SelectedOption = NormalizedOption | NormalizedOption[] | undefined;
10 |
11 | export default function useSelectableOption(
12 | options: Ref,
13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
14 | localValue: Ref,
15 | multiple: Ref,
16 | ): {
17 | selectedOption: Ref,
18 | hasSelectedOption: ComputedRef,
19 | selectOption: (option: NormalizedOption) => void,
20 | toggleOption: (option: NormalizedOption) => void,
21 | optionIsSelected: (option: NormalizedOption) => boolean,
22 | } {
23 | const optionIsSelected = (option: NormalizedOption): boolean => {
24 | if (multiple.value === true) {
25 | return Array.isArray(localValue.value)
26 | && localValue.value.some((value) => isEqual(value, option.value));
27 | }
28 |
29 | return isEqual(localValue.value, option.value);
30 | };
31 |
32 | const getSelectedOption = (currentSelectedOption?: SelectedOption): SelectedOption => {
33 | let allOptions: NormalizedOption[] = options.value;
34 |
35 | // If the option is part of the current selected option is desired to use
36 | // those option since its possible that the options is not in the list
37 | // For example: an option that was selected from an ajax list but was removed
38 | if (Array.isArray(currentSelectedOption)) {
39 | allOptions = allOptions
40 | // Remove the options that are also on the current selected option list
41 | .filter((option) => !currentSelectedOption.some((selectedOption) => isEqual(selectedOption.value, option.value)))
42 | // Concat the current selected option list
43 | .concat(currentSelectedOption);
44 | } else if (currentSelectedOption !== undefined) {
45 | allOptions = allOptions
46 | // Remove the selected option if already exists in the list so it
47 | // can be replaced with the selected option
48 | .filter((option) => !isEqual(currentSelectedOption.value, option.value))
49 | .concat([currentSelectedOption]);
50 | }
51 |
52 | if (multiple.value === true) {
53 | if (Array.isArray(localValue.value)) {
54 | return allOptions.filter((option) => optionIsSelected(option));
55 | }
56 |
57 | return [];
58 | }
59 |
60 | return allOptions.find((option) => optionIsSelected(option));
61 | };
62 |
63 | const selectedOption = ref(getSelectedOption());
64 |
65 | const selectOption = (option: NormalizedOption): void => {
66 | if (optionIsSelected(option)) {
67 | return;
68 | }
69 |
70 | if (multiple.value === true) {
71 | if (Array.isArray(localValue.value)) {
72 | localValue.value = addToArray(localValue.value, option.value);
73 | selectedOption.value = addToArray(selectedOption.value, option);
74 | } else {
75 | localValue.value = [option.value];
76 | selectedOption.value = [option];
77 | }
78 | } else {
79 | localValue.value = option.value;
80 | selectedOption.value = option;
81 | }
82 | };
83 |
84 | const toggleOption = (option: NormalizedOption): void => {
85 | if (optionIsSelected(option)) {
86 | if (multiple.value === true) {
87 | localValue.value = substractFromArray(localValue.value, option.value);
88 | } else {
89 | localValue.value = undefined;
90 | }
91 | } else if (multiple.value === true) {
92 | if (Array.isArray(localValue.value)) {
93 | localValue.value = addToArray(localValue.value, option.value);
94 | } else {
95 | localValue.value = [option.value];
96 | }
97 | } else {
98 | localValue.value = option.value;
99 | }
100 | };
101 |
102 | watch([options, localValue], () => {
103 | selectedOption.value = getSelectedOption(selectedOption.value);
104 | });
105 |
106 | const hasSelectedOption = computed((): boolean => {
107 | if (multiple.value === true) {
108 | return (selectedOption.value as NormalizedOption[]).length > 0;
109 | }
110 |
111 | return selectedOption.value !== undefined;
112 | });
113 |
114 | return {
115 | selectedOption,
116 | hasSelectedOption,
117 | selectOption,
118 | toggleOption,
119 | optionIsSelected,
120 | };
121 | }
122 |
--------------------------------------------------------------------------------
/src/components/TAlert.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
13 |
18 |
24 | {{ configuration.text }}
25 |
26 |
27 |
28 |
36 |
42 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
175 |
--------------------------------------------------------------------------------
/src/development/Modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Modal
5 |
6 |
7 |
11 |
12 |
13 | This it the header
14 |
15 |
19 | Hide modal
20 |
21 |
22 |
23 |
24 |
27 | Show modal
28 |
29 |
30 |
31 |
32 |
36 |
41 | User email is: {{ user.email }}
42 |
43 |
44 | set new email:
45 |
46 | write `cancel` to prevent the modal to hide
47 |
48 |
49 |
50 |
51 | set user email:
52 |
53 | write `cancel` to prevent the modal to show
54 |
55 |
56 |
57 |
60 | Show modal with parameter
61 |
62 |
63 |
64 |
65 |
70 |
71 |
72 | This it the header
73 |
74 |
78 | Hide modal
79 |
80 |
81 |
82 |
83 |
86 | Show modal
87 |
88 |
89 |
90 |
91 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | This it the header
102 |
103 | Lorem, ipsum dolor sit amet consectetur adipisicing elit. Repudiandae totam, alias voluptas deleniti ex tempora asperiores perspiciatis qui. Repellendus, exercitationem itaque! Beatae error ut fuga vel tempora, repellat optio molestiae!
104 |
105 | this is the footer
106 |
107 |
108 |
109 |
110 |
114 |
115 | Show modal
116 |
117 |
118 |
124 |
125 | hide()">
126 | Close this
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
183 |
--------------------------------------------------------------------------------
/src/__tests/use/useConfigurationWithClassesList.spec.ts:
--------------------------------------------------------------------------------
1 | import useConfigurationWithClassesList from '../../use/useConfigurationWithClassesList';
2 | import { useSetup } from './useSetup';
3 |
4 | describe('useConfigurationWithClassesList', () => {
5 | describe('configuration', () => {
6 | it('should keep the default configuration', () => {
7 | useSetup(() => {
8 | const { configuration } = useConfigurationWithClassesList({
9 | attrib: 'value',
10 | width: '10px',
11 | classes: {
12 | wrapper: 'border',
13 | body: 'text-base',
14 | },
15 | }, ['wrapper', 'body']);
16 |
17 | expect(configuration).toEqual({
18 | attrib: 'value',
19 | width: '10px',
20 | classesList: {
21 | wrapper: 'border',
22 | body: 'text-base',
23 | },
24 | });
25 | });
26 | });
27 |
28 | it('should merge the classes from the configuration', () => {
29 | useSetup(() => {
30 | const { configuration } = useConfigurationWithClassesList({
31 | classes: {
32 | wrapper: 'border',
33 | body: 'text-base',
34 | },
35 | fixedClasses: {
36 | wrapper: 'border-gray-200',
37 | body: 'p-3',
38 | },
39 | }, ['wrapper', 'body']);
40 | expect(configuration).toEqual({
41 | classesList: {
42 | wrapper: 'border border-gray-200',
43 | body: 'text-base p-3',
44 | },
45 | });
46 | });
47 | });
48 |
49 | it('should override the classes from the configuration variant', () => {
50 | useSetup(() => {
51 | const { configuration } = useConfigurationWithClassesList({
52 | classes: {
53 | wrapper: 'border',
54 | body: 'text-base',
55 | },
56 | fixedClasses: {
57 | wrapper: 'border-gray-200',
58 | body: 'p-3',
59 | },
60 | variants: {
61 | error: {
62 | classes: {
63 | wrapper: 'border-2',
64 | },
65 | },
66 | },
67 | variant: 'error',
68 | }, ['wrapper', 'body']);
69 | expect(configuration).toEqual({
70 | classesList: {
71 | wrapper: 'border-2 border-gray-200',
72 | body: 'text-base p-3',
73 | },
74 | });
75 | });
76 | });
77 |
78 | it('should merge the global configuration', () => {
79 | const globalConfiguration = {
80 | TCard: {
81 | placeholder: 'Hello world',
82 | classes: {
83 | wrapper: 'border',
84 | body: 'text-base',
85 | },
86 | },
87 | };
88 | useSetup(() => {
89 | const { configuration } = useConfigurationWithClassesList({
90 | maxlength: '2',
91 | }, ['wrapper', 'body']);
92 |
93 | expect(configuration).toEqual({
94 | maxlength: '2',
95 | placeholder: 'Hello world',
96 | classesList: {
97 | wrapper: 'border',
98 | body: 'text-base',
99 | },
100 | });
101 | }, globalConfiguration, {}, {}, 'TCard');
102 | });
103 |
104 | it('should use the default values from the props if not overriden', () => {
105 | const globalConfiguration = {};
106 | const attrs = {};
107 | const props = {
108 | body: {
109 | type: String,
110 | default: 'Hello world',
111 | },
112 | };
113 | useSetup(() => {
114 | const { configuration } = useConfigurationWithClassesList({
115 | maxlength: '2',
116 | }, ['wrapper', 'body']);
117 |
118 | expect(configuration).toEqual({
119 | maxlength: '2',
120 | body: 'Hello world',
121 | });
122 | }, globalConfiguration, attrs, props);
123 | });
124 | });
125 |
126 | describe('attributes', () => {
127 | it('contains the configuration the attributes', () => {
128 | useSetup(() => {
129 | const props = {
130 | placeholder: 'Hello World',
131 | };
132 |
133 | const { attributes } = useConfigurationWithClassesList(props, []);
134 |
135 | expect(attributes).toEqual({
136 | placeholder: 'Hello World',
137 | });
138 | }, {}, {});
139 | });
140 |
141 | it('adds the configurations attributes', () => {
142 | useSetup(() => {
143 | const props = {
144 | type: 'button',
145 | 'data-id': 'something',
146 | };
147 |
148 | const { attributes } = useConfigurationWithClassesList(props, []);
149 |
150 | expect(attributes).toEqual({
151 | type: 'button',
152 | 'data-id': 'something',
153 | });
154 | });
155 | });
156 |
157 | it('doesnt add the configurations attributes defined as a props', () => {
158 | useSetup(() => {
159 | const props = {
160 | type: 'button',
161 | 'data-id': 'something',
162 | };
163 | const { attributes } = useConfigurationWithClassesList(props, []);
164 |
165 | expect(attributes).toEqual({
166 | 'data-id': 'something',
167 | });
168 | }, {}, {}, ['type']);
169 | });
170 | });
171 | });
172 |
--------------------------------------------------------------------------------
/src/development/RichSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
13 |
14 |
15 |
19 |
27 |
28 |
29 |
33 |
41 |
42 |
43 |
47 |
55 |
56 |
57 |
61 |
67 |
68 |
69 |
73 |
78 |
79 |
80 |
81 |
82 |
178 |
--------------------------------------------------------------------------------