diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 1846284b..00000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -/dist/** -/docs/** -.eslintrc.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index a0662dcd..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2013-present, creativeLabs Lukasz Holeczek. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -'use strict' - -module.exports = { - root: true, // So parent files don't get applied - env: { - es6: true, - browser: true, - node: true, - }, - parser: '@typescript-eslint/parser', // Specifies the ESLint parser - parserOptions: { - ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features - sourceType: 'module', // Allows for the use of imports - extraFileExtensions: ['.vue'], - }, - extends: [ - 'eslint:recommended', - 'plugin:vue/vue3-recommended', - 'plugin:prettier/recommended', - 'plugin:unicorn/recommended', - '@vue/eslint-config-typescript/recommended', - '@vue/eslint-config-prettier', - ], - rules: { - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'unicorn/filename-case': 'off', - 'unicorn/no-array-for-each': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prefer-dom-node-append': 'off', - 'unicorn/prefer-export-from': 'off', - 'unicorn/prefer-query-selector': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'vue/require-default-prop': 'off', - }, - overrides: [ - { - files: ['**/*.mjs'], - env: { - browser: false, - node: true, - }, - parserOptions: { - sourceType: 'module', - }, - }, - { - files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], - env: { - jest: true, - }, - }, - { - files: ['packages/docs/build/**'], - env: { - browser: false, - node: true, - }, - parserOptions: { - sourceType: 'script', - }, - rules: { - 'no-console': 'off', - strict: 'error', - }, - }, - ], -} diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 415ca057..00000000 --- a/.prettierrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - semi: false, - trailingComma: "all", - singleQuote: true, - printWidth: 100, - tabWidth: 2 -}; \ No newline at end of file diff --git a/LICENSE b/LICENSE index f19fc729..fbb053e0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 creativeLabs Łukasz Holeczek +Copyright (c) 2025 creativeLabs Łukasz Holeczek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 31114030..86647480 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Several quick start options are available: -- [Download the latest release](https://github.com/coreui/coreui-vue/archive/v5.3.0.zip) +- [Download the latest release](https://github.com/coreui/coreui-vue/archive/v5.5.0.zip) - Clone the repo: `git clone https://github.com/coreui/coreui-vue.git` - Install with [npm](https://www.npmjs.com/): `npm install @coreui/vue` - Install with [yarn](https://yarnpkg.com/): `yarn add @coreui/vue` @@ -227,4 +227,4 @@ CoreUI is an MIT-licensed open source project and is completely free to use. How ## Copyright and license -Copyright 2024 creativeLabs Łukasz Holeczek. Code released under the [MIT License](https://github.com/coreui/coreui-vue/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). +Copyright 2025 creativeLabs Łukasz Holeczek. Code released under the [MIT License](https://github.com/coreui/coreui-vue/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..15164bf5 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,76 @@ +import eslint from '@eslint/js' +import eslintPluginUnicorn from 'eslint-plugin-unicorn' +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import eslintPluginVue from 'eslint-plugin-vue' +import globals from 'globals' +import typescriptEslint from 'typescript-eslint' + +export default typescriptEslint.config( + { ignores: ['**/*.d.ts', '**/coverage', '**/dist', '**/docs'] }, + { + extends: [ + eslint.configs.recommended, + ...typescriptEslint.configs.recommended, + ...eslintPluginVue.configs['flat/recommended'], + eslintPluginUnicorn.configs['flat/recommended'], + ], + files: ['packages/**/src/**/*.{js,ts,tsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.browser, + parserOptions: { + parser: typescriptEslint.parser, + }, + }, + rules: { + 'no-console': 'off', + 'no-debugger': 'off', + 'unicorn/filename-case': 'off', + 'unicorn/no-array-for-each': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prefer-dom-node-append': 'off', + 'unicorn/prefer-export-from': 'off', + 'unicorn/prefer-query-selector': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'vue/require-default-prop': 'off', + }, + }, + { + files: ['**/*.mjs'], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])), + ...globals.node, + }, + + ecmaVersion: 'latest', + sourceType: 'module', + }, + }, + { + files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], + languageOptions: { + globals: { + ...globals.jest, + }, + }, + }, + { + files: ['packages/docs/build/**'], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])), + ...globals.node, + }, + + ecmaVersion: 5, + sourceType: 'commonjs', + }, + rules: { + 'no-console': 'off', + strict: 'error', + }, + }, + eslintPluginPrettierRecommended, +) diff --git a/lerna.json b/lerna.json index 8e4098d4..8b1ec4d7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "npmClient": "yarn", "packages": ["packages/*"], - "version": "5.3.0", + "version": "5.5.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package.json b/package.json index 41fbecce..7b5343e7 100644 --- a/package.json +++ b/package.json @@ -17,23 +17,21 @@ "lib:build": "lerna run --scope \"@coreui/vue\" build --stream", "lib:test": "lerna run --scope \"@coreui/vue\" test --stream", "lib:test:update": "lerna run --scope \"@coreui/vue\" test:update --stream", - "lint": "eslint \"packages/**/src/**/*.{js,ts,tsx}\"", + "lint": "eslint", "test": "npm-run-all charts:test icons:test lib:test", "test:update": "npm-run-all charts:test:update icons:test:update lib:test:update" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.3.0", - "@typescript-eslint/parser": "^8.3.0", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/eslint-config-typescript": "^13.0.0", "@vue/vue3-jest": "29.2.6", - "eslint": "8.57.0", - "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-vue": "^9.27.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-unicorn": "^55.0.0", - "lerna": "^8.1.8", + "eslint": "^9.28.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-unicorn": "^59.0.1", + "eslint-plugin-vue": "^10.1.0", + "globals": "^16.2.0", + "lerna": "^8.2.2", "npm-run-all": "^4.1.5", - "prettier": "^3.3.3" + "prettier": "^3.5.3", + "typescript-eslint": "^8.33.1" } } diff --git a/packages/coreui-icons-vue b/packages/coreui-icons-vue index be8961e2..009583d5 160000 --- a/packages/coreui-icons-vue +++ b/packages/coreui-icons-vue @@ -1 +1 @@ -Subproject commit be8961e2115c0c86a1be72e4d0ea176c69ed4740 +Subproject commit 009583d58f7ea9a2a7bc7a2e62861eba48c0d911 diff --git a/packages/coreui-vue-chartjs b/packages/coreui-vue-chartjs index a3a42cab..f3b8364d 160000 --- a/packages/coreui-vue-chartjs +++ b/packages/coreui-vue-chartjs @@ -1 +1 @@ -Subproject commit a3a42cabd2b34dd1b1b78cbb72d5e6cdc41ba5cd +Subproject commit f3b8364d04dcd94c273ead1f740e1d792a5fb041 diff --git a/packages/coreui-vue/README.md b/packages/coreui-vue/README.md index 1ecdf9ed..a1cfd697 100644 --- a/packages/coreui-vue/README.md +++ b/packages/coreui-vue/README.md @@ -46,7 +46,7 @@ Several quick start options are available: -- [Download the latest release](https://github.com/coreui/coreui-vue/archive/v5.3.0.zip) +- [Download the latest release](https://github.com/coreui/coreui-vue/archive/v5.5.0.zip) - Clone the repo: `git clone https://github.com/coreui/coreui-vue.git` - Install with [npm](https://www.npmjs.com/): `npm install @coreui/vue` - Install with [yarn](https://yarnpkg.com/): `yarn add @coreui/vue` diff --git a/packages/coreui-vue/package.json b/packages/coreui-vue/package.json index 0bd4aee3..39b08b3a 100644 --- a/packages/coreui-vue/package.json +++ b/packages/coreui-vue/package.json @@ -1,6 +1,6 @@ { "name": "@coreui/vue", - "version": "5.3.0", + "version": "5.5.0", "description": "UI Components Library for Vue.js", "keywords": [ "vue", @@ -41,27 +41,27 @@ "test:update": "jest --coverage --updateSnapshot" }, "dependencies": { - "@coreui/coreui": "^5.1.2", + "@coreui/coreui": "^5.4.0", "@popperjs/core": "^2.11.8" }, "devDependencies": { - "@rollup/plugin-commonjs": "^26.0.1", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6", - "@types/jest": "^29.5.12", + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-typescript": "^12.1.2", + "@types/jest": "^29.5.14", "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "29.2.6", "cross-env": "^7.0.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "rollup": "^4.21.2", + "rollup": "^4.41.1", "rollup-plugin-vue": "^6.0.0", - "ts-jest": "^29.2.5", - "typescript": "^5.5.4", - "vue": "^3.4.38", - "vue-types": "^5.1.3" + "ts-jest": "^29.3.4", + "typescript": "^5.8.3", + "vue": "^3.5.16", + "vue-types": "^6.0.0" }, "peerDependencies": { - "vue": "^3.2.21" + "vue": "^3.5.0" } } diff --git a/packages/coreui-vue/src/components/accordion/CAccordionBody.ts b/packages/coreui-vue/src/components/accordion/CAccordionBody.ts index 4a8d83ff..c8907021 100644 --- a/packages/coreui-vue/src/components/accordion/CAccordionBody.ts +++ b/packages/coreui-vue/src/components/accordion/CAccordionBody.ts @@ -4,11 +4,12 @@ import { CCollapse } from '../collapse/CCollapse' const CAccordionBody = defineComponent({ name: 'CAccordionBody', setup(_, { slots }) { + const id = inject('id') const visible = inject('visible') as Ref return () => h( CCollapse, - { class: 'accordion-collapse', visible: visible.value }, + { class: 'accordion-collapse', id, visible: visible.value }, { default: () => h('div', { class: ['accordion-body'] }, slots.default && slots.default()), }, diff --git a/packages/coreui-vue/src/components/accordion/CAccordionButton.ts b/packages/coreui-vue/src/components/accordion/CAccordionButton.ts index 69ab7a00..f9820b4a 100644 --- a/packages/coreui-vue/src/components/accordion/CAccordionButton.ts +++ b/packages/coreui-vue/src/components/accordion/CAccordionButton.ts @@ -3,6 +3,7 @@ import { defineComponent, h, inject, Ref } from 'vue' const CAccordionButton = defineComponent({ name: 'CAccordionButton', setup(_, { slots }) { + const id = inject('id') as string const toggleVisibility = inject('toggleVisibility') as () => void const visible = inject('visible') as Ref @@ -11,7 +12,8 @@ const CAccordionButton = defineComponent({ 'button', { type: 'button', - 'aria-expanded': !visible.value, + 'aria-control': id, + 'aria-expanded': visible.value, class: ['accordion-button', { ['collapsed']: !visible.value }], onClick: () => toggleVisibility(), }, diff --git a/packages/coreui-vue/src/components/accordion/CAccordionItem.ts b/packages/coreui-vue/src/components/accordion/CAccordionItem.ts index 20575085..f2ebe594 100644 --- a/packages/coreui-vue/src/components/accordion/CAccordionItem.ts +++ b/packages/coreui-vue/src/components/accordion/CAccordionItem.ts @@ -1,8 +1,12 @@ -import { defineComponent, h, inject, provide, ref, watch, Ref } from 'vue' +import { defineComponent, h, inject, provide, ref, watch, Ref, useId } from 'vue' const CAccordionItem = defineComponent({ name: 'CAccordionItem', props: { + /** + * The id global attribute defines an identifier (ID) that must be unique in the whole document. + */ + id: String, /** * The item key. */ @@ -13,16 +17,20 @@ const CAccordionItem = defineComponent({ const alwaysOpen = inject('alwaysOpen') as boolean const setActiveItemKey = inject('setActiveItemKey') as (key: number | string) => void - const itemKey = ref(props.itemKey ?? Math.random().toString(36).slice(2, 11)) + const id = props.id ?? useId() + const itemKey = ref(props.itemKey ?? id) const visible = ref(Boolean(activeItemKey.value === itemKey.value)) watch(activeItemKey, () => (visible.value = Boolean(activeItemKey.value === itemKey.value))) const toggleVisibility = () => { visible.value = !visible.value - !alwaysOpen && visible && setActiveItemKey(itemKey.value) + if (!alwaysOpen && visible) { + setActiveItemKey(itemKey.value) + } } + provide('id', id) provide('visible', visible) provide('toggleVisibility', toggleVisibility) diff --git a/packages/coreui-vue/src/components/button/CButton.ts b/packages/coreui-vue/src/components/button/CButton.ts index 287bfd12..70f7e90d 100644 --- a/packages/coreui-vue/src/components/button/CButton.ts +++ b/packages/coreui-vue/src/components/button/CButton.ts @@ -93,9 +93,9 @@ export const CButton = defineComponent({ { class: [ 'btn', + props.variant && props.color ? `btn-${props.variant}-${props.color}` : `btn-${props.variant}`, { [`btn-${props.color}`]: props.color && !props.variant, - [`btn-${props.variant}-${props.color}`]: props.color && props.variant, [`btn-${props.size}`]: props.size, active: props.active, disabled: props.disabled, diff --git a/packages/coreui-vue/src/components/form/CFormControlWrapper.ts b/packages/coreui-vue/src/components/form/CFormControlWrapper.ts index e4aee290..0ed3d195 100644 --- a/packages/coreui-vue/src/components/form/CFormControlWrapper.ts +++ b/packages/coreui-vue/src/components/form/CFormControlWrapper.ts @@ -4,11 +4,27 @@ import { CFormFloating } from './CFormFloating' import { CFormLabel } from './CFormLabel' import { CFormText } from './CFormText' +import type { ComponentProps } from '../../utils/ComponentProps' + +interface CFormControlWrapperProps extends ComponentProps { + floatingClassName?: string + floatingLabel?: string + id?: string + label?: string + text?: string +} + const CFormControlWrapper = defineComponent({ name: 'CFormControlWrapper', inheritAttrs: false, props: { ...CFormControlValidation.props, + /** + * A string of all className you want applied to the floating label wrapper. + * + * @since 5.5.0 + */ + floatingClassName: String, /** * Provide valuable, actionable valid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`. * @@ -18,7 +34,9 @@ const CFormControlWrapper = defineComponent({ /** * @ignore */ - id: String, + id: { + type: String, + }, /** * Add a caption for a component. * @@ -32,7 +50,7 @@ const CFormControlWrapper = defineComponent({ */ text: String, }, - setup(props, { slots }) { + setup(props: CFormControlWrapperProps, { slots }) { const formControlValidation = () => h( CFormControlValidation, @@ -41,7 +59,6 @@ const CFormControlWrapper = defineComponent({ feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, - floatingLabel: props.floatingLabel, invalid: props.invalid, tooltipFeedback: props.tooltipFeedback, valid: props.valid, @@ -59,29 +76,36 @@ const CFormControlWrapper = defineComponent({ return () => props.floatingLabel - ? h(CFormFloating, () => [ - slots.default && slots.default(), - h( - CFormLabel, - { - for: props.id, - }, - { - default: () => (slots.label && slots.label()) || props.label || props.floatingLabel, - }, - ), - (props.text || slots.text) && + ? h( + CFormFloating, + { + class: props.floatingClassName, + }, + () => [ + slots.default && slots.default(), h( - CFormText, + CFormLabel, { - id: props.describedby, + for: props.id, }, { - default: () => (slots.text && slots.text()) || props.text, + default: () => + (slots.label && slots.label()) || props.label || props.floatingLabel, }, ), - formControlValidation(), - ]) + (props.text || slots.text) && + h( + CFormText, + { + id: props.describedby, + }, + { + default: () => (slots.text && slots.text()) || props.text, + }, + ), + formControlValidation(), + ], + ) : [ (props.label || slots.label) && h( diff --git a/packages/coreui-vue/src/components/form/CFormInput.ts b/packages/coreui-vue/src/components/form/CFormInput.ts index 68746233..8dbe4eca 100644 --- a/packages/coreui-vue/src/components/form/CFormInput.ts +++ b/packages/coreui-vue/src/components/form/CFormInput.ts @@ -130,7 +130,9 @@ const CFormInput = defineComponent({ h( CFormControlWrapper, { - describedby: attrs['aria-describedby'], + ...(typeof attrs['aria-describedby'] === 'string' && { + describedby: attrs['aria-describedby'], + }), feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, diff --git a/packages/coreui-vue/src/components/form/CFormSelect.ts b/packages/coreui-vue/src/components/form/CFormSelect.ts index b3086b60..23bd7b1b 100644 --- a/packages/coreui-vue/src/components/form/CFormSelect.ts +++ b/packages/coreui-vue/src/components/form/CFormSelect.ts @@ -119,7 +119,9 @@ const CFormSelect = defineComponent({ h( CFormControlWrapper, { - describedby: attrs['aria-describedby'], + ...(typeof attrs['aria-describedby'] === 'string' && { + describedby: attrs['aria-describedby'], + }), feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, diff --git a/packages/coreui-vue/src/components/form/CFormTextarea.ts b/packages/coreui-vue/src/components/form/CFormTextarea.ts index a953991d..42a8a732 100644 --- a/packages/coreui-vue/src/components/form/CFormTextarea.ts +++ b/packages/coreui-vue/src/components/form/CFormTextarea.ts @@ -106,7 +106,9 @@ const CFormTextarea = defineComponent({ h( CFormControlWrapper, { - describedby: attrs['aria-describedby'], + ...(typeof attrs['aria-describedby'] === 'string' && { + describedby: attrs['aria-describedby'], + }), feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, diff --git a/packages/coreui-vue/src/components/nav/CNav.ts b/packages/coreui-vue/src/components/nav/CNav.ts index 922603e9..6b5b7384 100644 --- a/packages/coreui-vue/src/components/nav/CNav.ts +++ b/packages/coreui-vue/src/components/nav/CNav.ts @@ -24,12 +24,12 @@ const CNav = defineComponent({ /** * Set the nav variant to tabs or pills. * - * @values 'pills', 'tabs', 'underline', 'underline-border' + * @values 'enclosed', 'enclosed-pills', 'pills', 'tabs', 'underline', 'underline-border' */ variant: { type: String, validator: (value: string) => { - return ['pills', 'tabs', 'underline', 'underline-border'].includes(value) + return ['enclosed', 'enclosed-pills', 'pills', 'tabs', 'underline', 'underline-border'].includes(value) }, }, }, @@ -40,6 +40,7 @@ const CNav = defineComponent({ { class: [ 'nav', + props.variant === 'enclosed-pills' && 'nav-enclosed', { [`nav-${props.layout}`]: props.layout, [`nav-${props.variant}`]: props.variant, diff --git a/packages/coreui-vue/src/components/nav/CNavGroup.ts b/packages/coreui-vue/src/components/nav/CNavGroup.ts index 2c83ccdf..7c45bd7c 100644 --- a/packages/coreui-vue/src/components/nav/CNavGroup.ts +++ b/packages/coreui-vue/src/components/nav/CNavGroup.ts @@ -41,7 +41,10 @@ const CNavGroup = defineComponent({ onMounted(() => { visible.value = props.visible - props.visible && navGroupRef.value.classList.add('show') + if (props.visible) { + navGroupRef.value.classList.add('show') + } + emit('visible-change', visible.value) }) @@ -60,7 +63,8 @@ const CNavGroup = defineComponent({ emit('visible-change', visible.value) }) - const handleTogglerClick = () => { + const handleTogglerClick = (event: Event) => { + event.preventDefault() visible.value = !visible.value emit('visible-change', visible.value) } @@ -111,6 +115,7 @@ const CNavGroup = defineComponent({ 'a', { class: ['nav-link', 'nav-group-toggle'], + href: '#', onClick: handleTogglerClick, }, slots.togglerContent && slots.togglerContent(), diff --git a/packages/coreui-vue/src/components/nav/CNavItem.ts b/packages/coreui-vue/src/components/nav/CNavItem.ts index 3ed139c7..e660c01a 100644 --- a/packages/coreui-vue/src/components/nav/CNavItem.ts +++ b/packages/coreui-vue/src/components/nav/CNavItem.ts @@ -1,11 +1,21 @@ import { defineComponent, h } from 'vue' - import { CNavLink } from './CNavLink' +import type { ComponentProps } from '../../utils/ComponentProps' + +interface CNavItemProps extends ComponentProps { + as: string + class: string +} + const CNavItem = defineComponent({ name: 'CNavItem', + inheritAttrs: false, props: { - ...CNavLink.props, + /** + * Toggle the active state for the component. + */ + active: Boolean, /** * Component used for the root node. Either a string to use a HTML element or a component. */ @@ -13,19 +23,31 @@ const CNavItem = defineComponent({ type: String, default: 'li', }, + /** + * A string of all className you want applied to the component. + */ + class: String, + /** + * Toggle the disabled state for the component. + */ + disabled: Boolean, + /** + * @ignore + */ + href: String, }, - setup(props, { slots }) { + setup(props: CNavItemProps, { attrs, slots }) { return () => h( props.as, { - as: props.component, - class: 'nav-item', + class: ['nav-item', props.class], }, props.href ? h( CNavLink, { + ...attrs, active: props.active, disabled: props.disabled, href: props.href, diff --git a/packages/coreui-vue/src/components/popover/CPopover.ts b/packages/coreui-vue/src/components/popover/CPopover.ts index 03ade861..300f876a 100644 --- a/packages/coreui-vue/src/components/popover/CPopover.ts +++ b/packages/coreui-vue/src/components/popover/CPopover.ts @@ -1,8 +1,8 @@ -import { defineComponent, h, onMounted, PropType, ref, RendererElement, Transition } from 'vue' +import { defineComponent, h, PropType, ref, RendererElement, Transition, useId } from 'vue' import type { Placement } from '@popperjs/core' import { CConditionalTeleport } from '../conditional-teleport' -import { useUniqueId, usePopper } from '../../composables' +import { usePopper } from '../../composables' import type { Placements, Triggers } from '../../types' import { executeAfterTransition } from '../../utils/transition' import { getRTLPlacement } from '../../utils' @@ -117,10 +117,10 @@ const CPopover = defineComponent({ setup(props, { attrs, slots, emit }) { const togglerRef = ref() const popoverRef = ref() - const uID = ref() const visible = ref(props.visible) - const { getUID } = useUniqueId('popover') + const { initPopper, destroyPopper } = usePopper() + const uniqueId = `popover-${useId()}` const delay = typeof props.delay === 'number' ? { show: props.delay, hide: props.delay } : props.delay @@ -149,10 +149,6 @@ const CPopover = defineComponent({ placement: getRTLPlacement(props.placement, togglerRef.value), } - onMounted(() => { - uID.value = getUID() - }) - const handleEnter = (el: RendererElement, done: () => void) => { emit('show') initPopper(togglerRef.value, popoverRef.value, popperConfig) @@ -212,7 +208,7 @@ const CPopover = defineComponent({ }, attrs.class, ], - id: uID.value, + id: uniqueId, ref: popoverRef, role: 'tooltip', }, @@ -241,7 +237,7 @@ const CPopover = defineComponent({ ), slots.toggler && slots.toggler({ - id: visible.value ? uID.value : null, + id: visible.value ? uniqueId : null, on: { click: (event: Event) => props.trigger.includes('click') && toggleVisible(event, !visible.value), diff --git a/packages/coreui-vue/src/components/tabs/CTab.ts b/packages/coreui-vue/src/components/tabs/CTab.ts index 05ce3fe4..5eb4c513 100644 --- a/packages/coreui-vue/src/components/tabs/CTab.ts +++ b/packages/coreui-vue/src/components/tabs/CTab.ts @@ -3,6 +3,12 @@ import { defineComponent, h, inject, Ref } from 'vue' const CTab = defineComponent({ name: 'CTab', props: { + /** + * Toggle the disabled state for the component. + * + * @since 5.4.0 + */ + disabled: Boolean, /** * Item key. */ @@ -13,7 +19,7 @@ const CTab = defineComponent({ }, setup(props, { slots }) { const activeItemKey = inject('activeItemKey') as Ref - const id = inject('id') as Ref + const id = inject('id') const setActiveItemKey = inject('setActiveItemKey') as (key: number | string) => void const isActive = () => props.itemKey === activeItemKey.value @@ -28,12 +34,13 @@ const CTab = defineComponent({ active: isActive(), }, ], - id: `${props.itemKey}-tab-${id.value}`, + id: `${props.itemKey}-tab-${id}`, role: 'tab', tabindex: isActive() ? 0 : -1, type: 'button', - 'aria-controls': `${props.itemKey}-tab-panel-${id.value}`, + 'aria-controls': `${props.itemKey}-tab-panel-${id}`, 'aria-selected': isActive(), + disabled: props.disabled, onClick: () => setActiveItemKey(props.itemKey), onFocus: () => setActiveItemKey(props.itemKey), }, diff --git a/packages/coreui-vue/src/components/tabs/CTabList.ts b/packages/coreui-vue/src/components/tabs/CTabList.ts index 63b720f2..d73bb17d 100644 --- a/packages/coreui-vue/src/components/tabs/CTabList.ts +++ b/packages/coreui-vue/src/components/tabs/CTabList.ts @@ -18,12 +18,12 @@ const CTabList = defineComponent({ /** * Set the nav variant to tabs or pills. * - * @values 'pills', 'tabs', 'underline', 'underline-border' + * @values 'enclosed', 'enclosed-pills', 'pills', 'tabs', 'underline', 'underline-border' */ variant: { type: String, validator: (value: string) => { - return ['pills', 'tabs', 'underline', 'underline-border'].includes(value) + return ['enclosed', 'enclosed-pills', 'pills', 'tabs', 'underline', 'underline-border'].includes(value) }, }, }, @@ -72,6 +72,7 @@ const CTabList = defineComponent({ { class: [ 'nav', + props.variant === 'enclosed-pills' && 'nav-enclosed', { [`nav-${props.layout}`]: props.layout, [`nav-${props.variant}`]: props.variant, diff --git a/packages/coreui-vue/src/components/tabs/CTabPanel.ts b/packages/coreui-vue/src/components/tabs/CTabPanel.ts index a4d23e7d..3182e74f 100644 --- a/packages/coreui-vue/src/components/tabs/CTabPanel.ts +++ b/packages/coreui-vue/src/components/tabs/CTabPanel.ts @@ -50,8 +50,7 @@ const CTabPanel = defineComponent({ ], setup(props, { slots, emit }) { const activeItemKey = inject('activeItemKey') as Ref - const id = inject('id') as Ref - + const id = inject('id') const tabPaneRef = ref() const firstRender = ref(true) const visible = ref() @@ -112,9 +111,9 @@ const CTabPanel = defineComponent({ show: firstRender.value && visible.value, }, ], - id: `${props.itemKey}-tab-panel-${id.value}`, + id: `${props.itemKey}-tab-panel-${id}`, role: 'tabpanel', - 'aria-labelledby': `${props.itemKey}-tab-${id.value}`, + 'aria-labelledby': `${props.itemKey}-tab-${id}`, tabindex: 0, ref: tabPaneRef, }, diff --git a/packages/coreui-vue/src/components/tabs/CTabs.ts b/packages/coreui-vue/src/components/tabs/CTabs.ts index 112decfe..b61908c2 100644 --- a/packages/coreui-vue/src/components/tabs/CTabs.ts +++ b/packages/coreui-vue/src/components/tabs/CTabs.ts @@ -1,5 +1,4 @@ -import { defineComponent, h, provide, ref, watch } from 'vue' -import { useUniqueId } from '../../composables' +import { defineComponent, h, provide, ref, useId, watch } from 'vue' const CTabs = defineComponent({ name: 'CTabs', @@ -19,9 +18,8 @@ const CTabs = defineComponent({ 'change', ], setup(props, { slots, emit }) { - const { getUID } = useUniqueId() - const uID = ref(getUID()) const activeItemKey = ref(props.activeItemKey) + const uniqueId = useId() const setActiveItemKey = (key: string | number) => { activeItemKey.value = key } @@ -35,7 +33,7 @@ const CTabs = defineComponent({ ) provide('activeItemKey', activeItemKey) - provide('id', uID) + provide('id', uniqueId) provide('setActiveItemKey', setActiveItemKey) return () => h('div', { class: 'tabs' }, slots.default && slots.default()) diff --git a/packages/coreui-vue/src/components/toast/CToastClose.ts b/packages/coreui-vue/src/components/toast/CToastClose.ts index b00e8dd1..a190e323 100644 --- a/packages/coreui-vue/src/components/toast/CToastClose.ts +++ b/packages/coreui-vue/src/components/toast/CToastClose.ts @@ -1,6 +1,12 @@ -import { defineComponent, h, inject } from 'vue' +import { defineComponent, h, inject, resolveComponent } from 'vue' import { CCloseButton } from '../close-button/CCloseButton' +import type { ComponentProps } from '../../utils/ComponentProps' + +interface CCloseButtonProps extends ComponentProps { + as?: string +} + const CToastClose = defineComponent({ name: 'CToastClose', props: { @@ -16,7 +22,7 @@ const CToastClose = defineComponent({ */ 'close', ], - setup(props, { slots, emit }) { + setup(props: CCloseButtonProps, { slots, emit }) { // eslint-disable-next-line no-unused-vars const updateVisible = inject('updateVisible') as (visible: boolean) => void const handleClose = () => { @@ -26,7 +32,7 @@ const CToastClose = defineComponent({ return () => props.as ? h( - props.as, + resolveComponent(props.as), { onClick: () => { handleClose() diff --git a/packages/coreui-vue/src/components/toast/CToastHeader.ts b/packages/coreui-vue/src/components/toast/CToastHeader.ts index 74fbc651..3f822da0 100644 --- a/packages/coreui-vue/src/components/toast/CToastHeader.ts +++ b/packages/coreui-vue/src/components/toast/CToastHeader.ts @@ -9,20 +9,11 @@ const CToastHeader = defineComponent({ */ closeButton: Boolean, }, - emits: [ - /** - * Event called after clicking the close button. - */ - 'close', - ], - setup(props, { slots, emit }) { + setup(props, { slots }) { return () => h('div', { class: 'toast-header' }, [ slots.default && slots.default(), - props.closeButton && - h(CToastClose, { - onClose: () => emit('close'), - }), + props.closeButton && h(CToastClose), ]) }, }) diff --git a/packages/coreui-vue/src/components/tooltip/CTooltip.ts b/packages/coreui-vue/src/components/tooltip/CTooltip.ts index 02ad416e..6fe15d0e 100644 --- a/packages/coreui-vue/src/components/tooltip/CTooltip.ts +++ b/packages/coreui-vue/src/components/tooltip/CTooltip.ts @@ -1,8 +1,8 @@ -import { defineComponent, h, onMounted, PropType, ref, RendererElement, Transition } from 'vue' +import { defineComponent, h, PropType, ref, RendererElement, Transition, useId } from 'vue' import type { Placement } from '@popperjs/core' import { CConditionalTeleport } from '../conditional-teleport' -import { useUniqueId, usePopper } from '../../composables' +import { usePopper } from '../../composables' import type { Placements, Triggers } from '../../types' import { executeAfterTransition } from '../../utils/transition' import { getRTLPlacement } from '../../utils' @@ -113,10 +113,10 @@ const CTooltip = defineComponent({ setup(props, { attrs, slots, emit }) { const togglerRef = ref() const tooltipRef = ref() - const uID = ref() const visible = ref(props.visible) - const { getUID } = useUniqueId('popover') + const { initPopper, destroyPopper } = usePopper() + const uniqueId = `tooltip-${useId()}` const delay = typeof props.delay === 'number' ? { show: props.delay, hide: props.delay } : props.delay @@ -145,10 +145,6 @@ const CTooltip = defineComponent({ placement: getRTLPlacement(props.placement, togglerRef.value), } - onMounted(() => { - uID.value = getUID() - }) - const handleEnter = (el: RendererElement, done: () => void) => { emit('show') initPopper(togglerRef.value, tooltipRef.value, popperConfig) @@ -208,7 +204,7 @@ const CTooltip = defineComponent({ }, attrs.class, ], - id: uID.value, + id: uniqueId, ref: tooltipRef, role: 'tooltip', }, @@ -229,7 +225,7 @@ const CTooltip = defineComponent({ ), slots.toggler && slots.toggler({ - id: visible.value ? uID.value : null, + id: visible.value ? uniqueId : null, on: { click: (event: Event) => props.trigger.includes('click') && toggleVisible(event, !visible.value), diff --git a/packages/coreui-vue/src/composables/useUniqueId.ts b/packages/coreui-vue/src/composables/useUniqueId.ts index 95810c31..e6f724a9 100644 --- a/packages/coreui-vue/src/composables/useUniqueId.ts +++ b/packages/coreui-vue/src/composables/useUniqueId.ts @@ -7,7 +7,7 @@ export const useUniqueId = (prefix: string = '') => { do { prefix += Math.floor(Math.random() * 1_000_000) } while (ids.value.includes(prefix)) - + ids.value.push(prefix) return prefix } diff --git a/packages/coreui-vue/src/utils/ComponentProps.ts b/packages/coreui-vue/src/utils/ComponentProps.ts new file mode 100644 index 00000000..5bd3991c --- /dev/null +++ b/packages/coreui-vue/src/utils/ComponentProps.ts @@ -0,0 +1,6 @@ +import { DefineComponent, ExtractPropTypes, ExtractPublicPropTypes } from 'vue' + +export type ComponentProps = + T extends DefineComponent, any, any> + ? ExtractPublicPropTypes + : never diff --git a/packages/docs/.prettierrc b/packages/docs/.prettierrc new file mode 100644 index 00000000..53e4559d --- /dev/null +++ b/packages/docs/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2 + +} diff --git a/packages/docs/api/accordion/CAccordionItem.api.md b/packages/docs/api/accordion/CAccordionItem.api.md index 753acd83..3e07d5aa 100644 --- a/packages/docs/api/accordion/CAccordionItem.api.md +++ b/packages/docs/api/accordion/CAccordionItem.api.md @@ -8,6 +8,7 @@ import CAccordionItem from '@coreui/vue/src/components/accordion/CAccordionItem' #### Props -| Prop name | Description | Type | Values | Default | -| ------------ | ------------- | -------------- | ------ | ------- | -| **item-key** | The item key. | number\|string | - | - | +| Prop name | Description | Type | Values | Default | +| ------------ | --------------------------------------------------------------------------------------------- | -------------- | ------ | ------- | +| **id** | The id global attribute defines an identifier (ID) that must be unique in the whole document. | string | - | - | +| **item-key** | The item key. | number\|string | - | - | diff --git a/packages/docs/api/form/CFormControlWrapper.api.md b/packages/docs/api/form/CFormControlWrapper.api.md index c4d97891..ebce3a46 100644 --- a/packages/docs/api/form/CFormControlWrapper.api.md +++ b/packages/docs/api/form/CFormControlWrapper.api.md @@ -8,8 +8,9 @@ import CFormControlWrapper from '@coreui/vue/src/components/form/CFormControlWra #### Props -| Prop name | Description | Type | Values | Default | -| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------- | -| **floating-label**
4.3.0+
| Provide valuable, actionable valid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`. | string | - | - | -| **label**
4.3.0+
| Add a caption for a component. | string | - | - | -| **text**
4.3.0+
| Add helper text to the component. | string | - | - | +| Prop name | Description | Type | Values | Default | +| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------- | +| **floating-class-name**
5.5.0+
| A string of all className you want applied to the floating label wrapper. | string | - | - | +| **floating-label**
4.3.0+
| Provide valuable, actionable valid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`. | string | - | - | +| **label**
4.3.0+
| Add a caption for a component. | string | - | - | +| **text**
4.3.0+
| Add helper text to the component. | string | - | - | diff --git a/packages/docs/api/nav/CNav.api.md b/packages/docs/api/nav/CNav.api.md index deda7877..37c9f02c 100644 --- a/packages/docs/api/nav/CNav.api.md +++ b/packages/docs/api/nav/CNav.api.md @@ -8,8 +8,8 @@ import CNav from '@coreui/vue/src/components/nav/CNav' #### Props -| Prop name | Description | Type | Values | Default | -| ----------- | --------------------------------------------------------------------------------------- | ------ | -------------------------------------------------------- | ------- | -| **as** | Component used for the root node. Either a string to use a HTML element or a component. | string | - | 'ul' | -| **layout** | Specify a layout type for component. | string | `'fill'`, `'justified'` | - | -| **variant** | Set the nav variant to tabs or pills. | string | `'pills'`, `'tabs'`, `'underline'`, `'underline-border'` | - | +| Prop name | Description | Type | Values | Default | +| ----------- | --------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------ | ------- | +| **as** | Component used for the root node. Either a string to use a HTML element or a component. | string | - | 'ul' | +| **layout** | Specify a layout type for component. | string | `'fill'`, `'justified'` | - | +| **variant** | Set the nav variant to tabs or pills. | string | `'enclosed'`, `'enclosed-pills'`, `'pills'`, `'tabs'`, `'underline'`, `'underline-border'` | - | diff --git a/packages/docs/api/nav/CNavItem.api.md b/packages/docs/api/nav/CNavItem.api.md index 2a2d63d1..788b3792 100644 --- a/packages/docs/api/nav/CNavItem.api.md +++ b/packages/docs/api/nav/CNavItem.api.md @@ -8,6 +8,9 @@ import CNavItem from '@coreui/vue/src/components/nav/CNavItem' #### Props -| Prop name | Description | Type | Values | Default | -| --------- | --------------------------------------------------------------------------------------- | ------ | ------ | ------- | -| **as** | Component used for the root node. Either a string to use a HTML element or a component. | string | - | 'li' | +| Prop name | Description | Type | Values | Default | +| ------------ | --------------------------------------------------------------------------------------- | ------- | ------ | ------- | +| **active** | Toggle the active state for the component. | boolean | - | - | +| **as** | Component used for the root node. Either a string to use a HTML element or a component. | string | - | 'li' | +| **class** | A string of all className you want applied to the component. | string | - | - | +| **disabled** | Toggle the disabled state for the component. | boolean | - | - | diff --git a/packages/docs/api/stepper/CStepper.api.md b/packages/docs/api/stepper/CStepper.api.md new file mode 100644 index 00000000..e13e9c9a --- /dev/null +++ b/packages/docs/api/stepper/CStepper.api.md @@ -0,0 +1,22 @@ +### CStepper + +```jsx +import { CStepper } from '@coreui/vue' +// or +import CStepper from '@coreui/vue/src/components/stepper/CStepper' +``` + +#### Props + +| Prop name | Description | Type | Values | Default | +| --------- | ----------- | ---- | ------ | ------- | + +#### Events + +| Event name | Description | Properties | +| ---------------------------- | ----------- | ---------- | +| **update:modelValue** | | +| **finish** | | +| **reset** | | +| **step-change** | | +| **step-validation-complete** | | diff --git a/packages/docs/api/tabs/CTab.api.md b/packages/docs/api/tabs/CTab.api.md index ed8dc7b0..7cdff394 100644 --- a/packages/docs/api/tabs/CTab.api.md +++ b/packages/docs/api/tabs/CTab.api.md @@ -8,6 +8,7 @@ import CTab from '@coreui/vue/src/components/tabs/CTab' #### Props -| Prop name | Description | Type | Values | Default | -| ------------ | ----------- | -------------- | ------ | ------- | -| **item-key** | Item key. | number\|string | - | - | +| Prop name | Description | Type | Values | Default | +| ----------------------------------------------------------- | -------------------------------------------- | -------------- | ------ | ------- | +| **disabled**
5.4.0+
| Toggle the disabled state for the component. | boolean | - | - | +| **item-key** | Item key. | number\|string | - | - | diff --git a/packages/docs/api/tabs/CTabList.api.md b/packages/docs/api/tabs/CTabList.api.md index 68b9ca46..c12138af 100644 --- a/packages/docs/api/tabs/CTabList.api.md +++ b/packages/docs/api/tabs/CTabList.api.md @@ -8,7 +8,7 @@ import CTabList from '@coreui/vue/src/components/tabs/CTabList' #### Props -| Prop name | Description | Type | Values | Default | -| ----------- | ------------------------------------- | ------ | -------------------------------------------------------- | ------- | -| **layout** | Specify a layout type for component. | string | `'fill'`, `'justified'` | - | -| **variant** | Set the nav variant to tabs or pills. | string | `'pills'`, `'tabs'`, `'underline'`, `'underline-border'` | - | +| Prop name | Description | Type | Values | Default | +| ----------- | ------------------------------------- | ------ | ------------------------------------------------------------------------------------------ | ------- | +| **layout** | Specify a layout type for component. | string | `'fill'`, `'justified'` | - | +| **variant** | Set the nav variant to tabs or pills. | string | `'enclosed'`, `'enclosed-pills'`, `'pills'`, `'tabs'`, `'underline'`, `'underline-border'` | - | diff --git a/packages/docs/api/toast/CToastHeader.api.md b/packages/docs/api/toast/CToastHeader.api.md index fdd97dc8..a8ff8638 100644 --- a/packages/docs/api/toast/CToastHeader.api.md +++ b/packages/docs/api/toast/CToastHeader.api.md @@ -11,9 +11,3 @@ import CToastHeader from '@coreui/vue/src/components/toast/CToastHeader' | Prop name | Description | Type | Values | Default | | ---------------- | ----------------------------------------------- | ------- | ------ | ------- | | **close-button** | Automatically add a close button to the header. | boolean | - | - | - -#### Events - -| Event name | Description | Properties | -| ---------- | --------------------------------------------- | ---------- | -| **close** | Event called after clicking the close button. | diff --git a/packages/docs/build/docgen.config.js b/packages/docs/build/docgen.config.js index e4c71f24..7d654712 100644 --- a/packages/docs/build/docgen.config.js +++ b/packages/docs/build/docgen.config.js @@ -8,7 +8,8 @@ module.exports = { components: [ '**/[A-Z]*.ts', '!**/[A-Z]*.d.ts', - '!**/[A-Z]*.spec.ts' + '!**/[A-Z]*.spec.ts', + '!**/ComponentProps.ts', ], outDir: 'api', // folder to save components docs in (relative to the current working directry) getDocFileName: (componentPath) => diff --git a/packages/docs/components/button.md b/packages/docs/components/button.md index 0f9a8428..fe592e77 100644 --- a/packages/docs/components/button.md +++ b/packages/docs/components/button.md @@ -63,7 +63,27 @@ If you're using `` component as `` elements that are used to trigger ## Outline buttons -If you need a button, but without the strong background colors. Set `variant="outline"` prop to remove all background colors. +### Base outline style + +The `variant="outline` property provides a neutral outline button style without any color modifiers. It’s useful as a foundation for minimal buttons without background color or strong visual emphasis. + +::: demo +Base outline button +Active state +Disabled state +::: +```vue +Base outline button +Active state +Disabled state +``` + +These Vue buttons use a transparent background, subtle border, and inherit text color from the parent context. They’re best suited for minimalist UI elements like modals, toolbars, or secondary actions. + + +### Themed outline variants + +If you need a button, but without the strong background colors, set `color` and `variant=" outline"` props to remove all background colors. ::: demo Primary @@ -86,10 +106,31 @@ If you need a button, but without the strong background colors. Set `variant="ou Dark ``` +These outline variants of our Vue.js buttons retain transparent backgrounds by default, but display a background tint on hover or focus to indicate interactivity. They’re ideal for secondary actions when you want to differentiate from the standard buttons visually. + ## Ghost buttons +### Base ghost style + +Use the `variant="ghost"` property to create ultra-minimalist buttons with no borders and a fully transparent background. These Vue buttons rely solely on text color for visibility and apply a background highlight when hovered over or in an active state. + +They’re perfect for interfaces where you want buttons to be present but visually unobtrusive—such as action buttons in modals, cards, or toolbars. + If you need a ghost variant of button, set `variant="ghost"` prop to remove all background colors. +::: demo +Base ghost button +Active state +Disabled state +::: +```vue +Base ghost button +Active state +Disabled state +``` + +To apply theme colors to Vue ghost buttons, use the `color` and `variant="ghost"` properties. By default, these variants color only the text. On hover or focus, they add a background that corresponds to the theme color. + ::: demo Primary Secondary diff --git a/packages/docs/components/dropdown.md b/packages/docs/components/dropdown.md index da73b72e..cc321091 100644 --- a/packages/docs/components/dropdown.md +++ b/packages/docs/components/dropdown.md @@ -65,7 +65,7 @@ And with `` elements: The best part is you can do this with any button variant, too: ::: demo -