diff --git a/README.md b/README.md index 1f9f1834..e6c179ec 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ · Request feature · + Roadmap + · Blog

@@ -46,7 +48,7 @@ Several quick start options are available: -- [Download the latest release](https://github.com/coreui/coreui-vue/archive/v5.6.0.zip) +- [Download the latest release](https://github.com/coreui/coreui-vue/archive/v5.7.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` @@ -183,10 +185,11 @@ CoreUI supports most popular frameworks. Fully featured, out-of-the-box, templates for your application based on CoreUI. -- [Angular Admin Template](https://coreui.io/angular) -- [Bootstrap Admin Template](https://coreui.io/) -- [React Admin Template](https://coreui.io/react) -- [Vue Admin Template](https://coreui.io/vue) +- [Angular Admin Templates](https://coreui.io/themes-templates/admin-dashboard/angular/) +- [Bootstrap Admin Templates](https://coreui.io/themes-templates/admin-dashboard/bootstrap/) +- [Next.js Admin Templates](https://coreui.io/themes-templates/admin-dashboard/next-js/) +- [React Admin Templates](https://coreui.io/themes-templates/admin-dashboard/react/) +- [Vue Admin Templates](https://coreui.io/themes-templates/admin-dashboard/vue/) ## Contributing @@ -198,9 +201,9 @@ Editor preferences are available in the [editor config](https://github.com/coreu Stay up to date on the development of CoreUI and reach out to the community with these helpful resources. -- Read and subscribe to [The Official CoreUI Blog](https://coreui.io/blog/). - -You can also follow [@core_ui on Twitter](https://twitter.com/core_ui). +- Read and subscribe to [The Official CoreUI Blog](https://coreui.io/blog). +- Follow [@core_ui on Twitter](https://x.com/core_ui). +- Discuss, ask questions, and more on [the community Discord](https://discord.gg/pQRWe5XdGm). ## Versioning diff --git a/lerna.json b/lerna.json index a4d781c9..e456a603 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "npmClient": "yarn", "packages": ["packages/*"], - "version": "5.6.0", + "version": "5.7.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package.json b/package.json index c862edad..345d3fae 100644 --- a/package.json +++ b/package.json @@ -23,15 +23,15 @@ }, "devDependencies": { "@vue/vue3-jest": "29.2.6", - "eslint": "^9.34.0", + "eslint": "^9.35.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-unicorn": "^60.0.0", + "eslint-plugin-unicorn": "^61.0.2", "eslint-plugin-vue": "^10.4.0", - "globals": "^16.3.0", - "lerna": "^8.2.3", + "globals": "^16.4.0", + "lerna": "^8.2.4", "npm-run-all": "^4.1.5", "prettier": "^3.6.2", - "typescript-eslint": "^8.41.0" + "typescript-eslint": "^8.44.0" } } diff --git a/packages/coreui-vue/README.md b/packages/coreui-vue/README.md index aabe8d99..6237b002 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.6.0.zip) +- [Download the latest release](https://github.com/coreui/coreui-vue/archive/v5.7.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 52f613fe..f932b6c5 100644 --- a/packages/coreui-vue/package.json +++ b/packages/coreui-vue/package.json @@ -1,6 +1,6 @@ { "name": "@coreui/vue", - "version": "5.6.0", + "version": "5.7.0", "description": "UI Components Library for Vue.js", "keywords": [ "vue", @@ -54,11 +54,11 @@ "cross-env": "^10.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "rollup": "^4.50.0", + "rollup": "^4.50.2", "rollup-plugin-vue": "^6.0.0", "ts-jest": "^29.4.1", "typescript": "^5.9.2", - "vue": "^3.5.20", + "vue": "^3.5.21", "vue-types": "^6.0.0" }, "peerDependencies": { diff --git a/packages/coreui-vue/src/components/dropdown/CDropdown.ts b/packages/coreui-vue/src/components/dropdown/CDropdown.ts index 997e39a3..d51b4c8e 100644 --- a/packages/coreui-vue/src/components/dropdown/CDropdown.ts +++ b/packages/coreui-vue/src/components/dropdown/CDropdown.ts @@ -1,4 +1,15 @@ -import { defineComponent, h, ref, provide, watch, PropType, onUnmounted, nextTick } from 'vue' +import { + computed, + defineComponent, + h, + nextTick, + onUnmounted, + provide, + PropType, + ref, + Ref, + watch, +} from 'vue' import type { Placement } from '@popperjs/core' import { usePopper } from '../../composables' @@ -6,7 +17,8 @@ import type { Triggers } from '../../types' import { getNextActiveElement, isRTL } from '../../utils' import type { Alignments } from './types' -import { getPlacement } from './utils' +import { getPlacement, getReferenceElement } from './utils' +import { CFocusTrap } from '../focus-trap' const CDropdown = defineComponent({ name: 'CDropdown', @@ -53,7 +65,7 @@ const CDropdown = defineComponent({ * - `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu. */ autoClose: { - type: [Boolean, String], + type: [Boolean, String] as PropType, default: true, validator: (value: boolean | string) => { return typeof value === 'boolean' || ['inside', 'outside'].includes(value) @@ -112,6 +124,21 @@ const CDropdown = defineComponent({ type: Boolean, default: true, }, + /** + * Sets the reference element for positioning the Vue Dropdown Menu. + * - `toggle` - The Vue Dropdown Toggle button (default). + * - `parent` - The Vue Dropdown wrapper element. + * - `HTMLElement` - A custom HTML element. + * - `Ref` - A custom reference element. + * + * @since 5.7.0 + */ + reference: { + type: [String, Object] as PropType< + 'parent' | 'toggle' | HTMLElement | Ref + >, + default: 'toggle', + }, /** * Generates dropdown menu using Teleport. * @@ -156,15 +183,16 @@ const CDropdown = defineComponent({ 'show', ], setup(props, { slots, emit }) { - const dropdownToggleRef = ref() - const dropdownMenuRef = ref() + const dropdownRef = ref(null) + const dropdownMenuRef = ref(null) + const dropdownToggleRef = ref(null) const pendingKeyDownEventRef = ref(null) const popper = ref(typeof props.alignment === 'object' ? false : props.popper) const visible = ref(props.visible) const { initPopper, destroyPopper } = usePopper() - const popperConfig = { + const popperConfig = computed(() => ({ modifiers: [ { name: 'offset', @@ -179,7 +207,7 @@ const CDropdown = defineComponent({ props.alignment, isRTL(dropdownMenuRef.value) ) as Placement, - } + })) watch( () => props.visible, @@ -190,11 +218,16 @@ const CDropdown = defineComponent({ watch(visible, () => { if (visible.value && dropdownToggleRef.value && dropdownMenuRef.value) { - if (popper.value) { - initPopper(dropdownToggleRef.value, dropdownMenuRef.value, popperConfig) + const referenceElement = getReferenceElement( + props.reference, + dropdownToggleRef, + dropdownRef + ) + if (referenceElement && popper.value) { + initPopper(referenceElement, dropdownMenuRef.value, popperConfig.value) } - window.addEventListener('mouseup', handleMouseUp) + window.addEventListener('click', handleClick) window.addEventListener('keyup', handleKeyup) dropdownToggleRef.value.addEventListener('keydown', handleKeydown) dropdownMenuRef.value.addEventListener('keydown', handleKeydown) @@ -205,15 +238,20 @@ const CDropdown = defineComponent({ pendingKeyDownEventRef.value = null }) } - + emit('show') return } - popper.value && destroyPopper() - window.removeEventListener('mouseup', handleMouseUp) + if (popper.value) { + destroyPopper() + } + + window.removeEventListener('click', handleClick) window.removeEventListener('keyup', handleKeyup) dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown) + dropdownToggleRef.value && + dropdownToggleRef.value.removeEventListener('keydown', handleKeydown) emit('hide') }) @@ -259,24 +297,36 @@ const CDropdown = defineComponent({ } } - const handleMouseUp = (event: Event) => { + const handleClick = (event: Event) => { if (!dropdownToggleRef.value || !dropdownMenuRef.value) { return } - if (dropdownToggleRef.value.contains(event.target as HTMLElement)) { + if ((event as MouseEvent).button === 2) { + return + } + + const composedPath = event.composedPath() + const isOnToggle = composedPath.includes(dropdownToggleRef.value) + const isOnMenu = composedPath.includes(dropdownMenuRef.value) + + if (isOnToggle) { + return + } + + const target = event.target as HTMLElement | null + const FORM_TAG_RE = /^(input|select|option|textarea|form|button|label)$/i + + if (isOnMenu && target && FORM_TAG_RE.test(target.tagName)) { return } if ( props.autoClose === true || - (props.autoClose === 'inside' && - dropdownMenuRef.value.contains(event.target as HTMLElement)) || - (props.autoClose === 'outside' && - !dropdownMenuRef.value.contains(event.target as HTMLElement)) + (props.autoClose === 'inside' && isOnMenu) || + (props.autoClose === 'outside' && !isOnMenu) ) { setVisible(false) - return } } @@ -299,22 +349,28 @@ const CDropdown = defineComponent({ provide('setVisible', setVisible) return () => - props.variant === 'input-group' - ? [slots.default && slots.default()] - : h( - 'div', - { - class: [ - props.variant === 'nav-item' ? 'nav-item dropdown' : props.variant, - props.direction === 'center' - ? 'dropdown-center' - : props.direction === 'dropup-center' - ? 'dropup dropup-center' - : props.direction, - ], - }, - slots.default && slots.default() - ) + h( + CFocusTrap, + { active: props.teleport && visible.value, additionalContainer: dropdownMenuRef }, + () => + props.variant === 'input-group' + ? [slots.default && slots.default()] + : h( + 'div', + { + class: [ + props.variant === 'nav-item' ? 'nav-item dropdown' : props.variant, + props.direction === 'center' + ? 'dropdown-center' + : props.direction === 'dropup-center' + ? 'dropup dropup-center' + : props.direction, + ], + ref: dropdownRef, + }, + slots.default && slots.default() + ) + ) }, }) diff --git a/packages/coreui-vue/src/components/dropdown/CDropdownToggle.ts b/packages/coreui-vue/src/components/dropdown/CDropdownToggle.ts index 0f9b7baa..fbda664a 100644 --- a/packages/coreui-vue/src/components/dropdown/CDropdownToggle.ts +++ b/packages/coreui-vue/src/components/dropdown/CDropdownToggle.ts @@ -74,6 +74,15 @@ const CDropdownToggle = defineComponent({ * Similarly, create split button dropdowns with virtually the same markup as single button dropdowns, but with the addition of `.dropdown-toggle-split` className for proper spacing around the dropdown caret. */ split: Boolean, + /** + * Screen reader label for split button dropdown toggle. + * + * @since 5.7.0 + */ + splitLabel: { + type: String, + default: 'Toggle Dropdown', + }, /** * Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them. * @@ -194,7 +203,7 @@ const CDropdownToggle = defineComponent({ }, () => props.split - ? h('span', { class: 'visually-hidden' }, 'Toggle Dropdown') + ? h('span', { class: 'visually-hidden' }, props.splitLabel) : slots.default && slots.default(), ) }, diff --git a/packages/coreui-vue/src/components/dropdown/utils.ts b/packages/coreui-vue/src/components/dropdown/utils.ts index c9659636..03c4c3a0 100644 --- a/packages/coreui-vue/src/components/dropdown/utils.ts +++ b/packages/coreui-vue/src/components/dropdown/utils.ts @@ -1,3 +1,4 @@ +import { Ref } from 'vue' import type { Placement } from '@popperjs/core' import type { Placements } from '../../types' import type { Alignments, Breakpoints } from './types' @@ -49,3 +50,23 @@ export const getPlacement = ( return _placement } + +export const getReferenceElement = ( + reference: 'parent' | 'toggle' | Ref | HTMLElement, + dropdownToggleRef: Ref, + dropdownRef: Ref +): HTMLElement | null => { + if (reference === 'parent') { + return dropdownRef.value + } + + if (reference instanceof HTMLElement) { + return reference + } + + if (reference instanceof Object && 'value' in reference) { + return reference.value + } + + return dropdownToggleRef.value +} diff --git a/packages/coreui-vue/src/components/nav/CNavItem.ts b/packages/coreui-vue/src/components/nav/CNavItem.ts index e660c01a..bbe535b3 100644 --- a/packages/coreui-vue/src/components/nav/CNavItem.ts +++ b/packages/coreui-vue/src/components/nav/CNavItem.ts @@ -5,7 +5,7 @@ import type { ComponentProps } from '../../utils/ComponentProps' interface CNavItemProps extends ComponentProps { as: string - class: string + class?: string } const CNavItem = defineComponent({ diff --git a/packages/docs/.vuepress/client.ts b/packages/docs/.vuepress/client.ts index 0404f0fd..ae44077a 100644 --- a/packages/docs/.vuepress/client.ts +++ b/packages/docs/.vuepress/client.ts @@ -3,7 +3,6 @@ import { defineClientConfig } from '@vuepress/client' import { CIcon, CIconSvg } from '@coreui/icons-vue' import CChartPlugin from '@coreui/vue-chartjs' import CoreuiVue from '@coreui/vue/src/' -import '@coreui/coreui/scss/coreui.scss' import '@coreui/chartjs/scss/coreui-chartjs.scss' import { @@ -21,6 +20,7 @@ import { cilCheckCircle, cilCloudDownload, cilContrast, + cilExternalLink, cilHandshake, cilInfo, cilLayers, @@ -51,6 +51,7 @@ export const icons = { cilCheckCircle, cilCloudDownload, cilContrast, + cilExternalLink, cilHandshake, cilInfo, cilLayers, diff --git a/packages/docs/.vuepress/src/client/components/Header.vue b/packages/docs/.vuepress/src/client/components/Header.vue index 17ee1090..04628a9a 100644 --- a/packages/docs/.vuepress/src/client/components/Header.vue +++ b/packages/docs/.vuepress/src/client/components/Header.vue @@ -1,4 +1,5 @@