UNPKG

@coreui/vue

Version:

UI Components Library for Vue.js

362 lines (330 loc) 10.2 kB
import { computed, defineComponent, h, inject, ref, type VNode } from 'vue' import { Color } from '../../props' import { chipSetContextKey, type ChipSetConfig } from '../chip-set/useChipSet' const CChip = defineComponent({ name: 'CChip', props: { /** * Toggle the active state for the component. */ active: Boolean, /** * Provides an accessible label for the remove button. */ ariaRemoveLabel: { type: String, default: 'Remove', }, /** * Component used for the root node. Either a string to use a HTML element or a component. */ as: { type: String, default: 'span', }, /** * Enables interactive hover styling and pointer cursor. */ clickable: Boolean, /** * Sets the color context of the component to one of CoreUI's themed colors. * * @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light' */ color: Color, /** * Toggle the disabled state for the component. * * Defaults to `undefined` so a chip can override the value inherited from a `CChipSet` (including turning it off). */ disabled: { type: Boolean, default: undefined, }, /** * Turns the component into a filter chip. A filter chip is selectable and shows a leading check icon while selected. * * Defaults to `undefined` so a chip can override the value inherited from a `CChipSet`. */ filter: { type: Boolean, default: undefined, }, /** * Displays a remove button inside the component. * * Defaults to `undefined` so a chip can override the value inherited from a `CChipSet` (including turning it off). */ removable: { type: Boolean, default: undefined, }, /** * Replaces the default remove icon with a custom icon node. */ removeIcon: { type: [String, Object], default: undefined, }, /** * Enables selectable behavior and keyboard toggle support. * * Defaults to `undefined` so a chip can override the value inherited from a `CChipSet`. */ selectable: { type: Boolean, default: undefined, }, /** * Controls the selected state of a selectable component. */ selected: { type: Boolean, default: undefined, }, /** * Replaces the default selected icon shown by a filter chip with a custom icon node. */ selectedIcon: { type: [String, Object], default: undefined, }, /** * Size the component small or large. * * @values 'sm', 'lg' */ size: { type: String, validator: (value: string) => { return ['sm', 'lg'].includes(value) }, }, /** * Sets the value associated with the component, used by `CChipSet` to track selection. */ value: { type: String, default: undefined, }, /** * Set the button variant to an outlined style. * * @values 'outline' */ variant: { type: String, validator: (value: string) => { return value === 'outline' }, }, }, emits: [ /** * Event called when the user clicks on the component. */ 'click', /** * Event called when the component becomes deselected. */ 'deselect', /** * Event called when the user presses a key. */ 'keydown', /** * Event called when the component requests removal. */ 'remove', /** * Event called when the component becomes selected. */ 'select', /** * Event called when the selected state changes. */ 'selected-change', ], setup(props, { attrs, emit, slots, expose }) { const chipRef = ref<HTMLElement>() const internalSelected = ref(false) // A chip placed inside a CChipSet / CChipInput reads its coordinated state // from the set context; standalone it manages its own. const chipSet = inject(chipSetContextKey, null) const config = <K extends keyof ChipSetConfig>(key: K): ChipSetConfig[K] | undefined => chipSet?.config.value[key] // A chip's own prop (when set) overrides the set-level config; otherwise it // inherits it. `??` makes this work because the overridable props default to // `undefined`, so an explicit `false` still wins over the set. const isDisabled = computed(() => Boolean(props.disabled ?? config('disabled'))) const isFilter = computed(() => Boolean(props.filter ?? config('filter'))) const isRemovable = computed(() => Boolean(props.removable ?? config('removable'))) // A filter chip is selectable by definition. const isSelectable = computed(() => Boolean((props.selectable ?? config('selectable')) || isFilter.value) ) const removeIcon = computed(() => props.removeIcon ?? config('removeIcon')) const selectedIcon = computed(() => props.selectedIcon ?? config('selectedIcon')) const ariaRemoveLabel = computed(() => props.ariaRemoveLabel) const coordinated = computed(() => chipSet !== null && props.value !== undefined) const selectedState = computed(() => { if (coordinated.value) { return chipSet!.isSelected(props.value as string) } return props.selected === undefined ? internalSelected.value : Boolean(props.selected) }) const isFocusable = computed(() => Boolean(!isDisabled.value && (isSelectable.value || isRemovable.value)) ) const setSelectableState = (nextSelected: boolean, event: MouseEvent | KeyboardEvent): void => { if (!isSelectable.value || isDisabled.value || nextSelected === selectedState.value) { return } if (coordinated.value) { chipSet!.toggleSelected(props.value as string, nextSelected, event) } else if (props.selected === undefined) { internalSelected.value = nextSelected } if (nextSelected) { emit('select', event) } else { emit('deselect', event) } emit('selected-change', nextSelected, event) } const toggleSelectedState = (event: MouseEvent | KeyboardEvent): void => { setSelectableState(!selectedState.value, event) } const handleRemove = (event: MouseEvent | KeyboardEvent): void => { if (coordinated.value) { chipSet!.removeChip(props.value as string, event) } emit('remove', event) } const handleRemoveClick = (event: MouseEvent): void => { event.stopPropagation() handleRemove(event) } const handleClick = (event: MouseEvent): void => { if (isDisabled.value) { return } if ((event.target as HTMLElement).closest('.chip-remove')) { return } if (isSelectable.value) { toggleSelectedState(event) } emit('click', event) } const handleKeydown = (event: KeyboardEvent): void => { if (isDisabled.value) { emit('keydown', event) return } switch (event.key) { case 'Enter': case ' ': case 'Spacebar': { if (isSelectable.value) { event.preventDefault() toggleSelectedState(event) } break } case 'Backspace': case 'Delete': { if (isRemovable.value) { event.preventDefault() handleRemove(event) } break } // No default } emit('keydown', event) } expose({ chipRef }) return () => { const removeIconVNode = slots.removeIcon?.() || removeIcon.value || h( 'svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap': 'round', }, [h('line', { x1: 4, y1: 4, x2: 12, y2: 12 }), h('line', { x1: 12, y1: 4, x2: 4, y2: 12 })] ) const selectedIconVNode = slots.selectedIcon?.() || selectedIcon.value || h( 'svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 512 512', fill: 'currentColor', }, h('path', { d: 'M425.373 89.373 196 318.745 86.627 209.373l-45.254 45.254L196 409.255l274.627-274.628z', }) ) const children = [ isFilter.value && selectedState.value && h('span', { class: 'chip-check', 'aria-hidden': 'true' }, selectedIconVNode as VNode), slots.default && slots.default(), isRemovable.value && !isDisabled.value && h( 'button', { type: 'button', class: 'chip-remove', 'aria-label': ariaRemoveLabel.value, onClick: handleRemoveClick, tabindex: -1, }, removeIconVNode as VNode ), ].filter(Boolean) return h( props.as, { ref: chipRef, class: [ 'chip', { active: isSelectable.value ? selectedState.value : props.active, disabled: isDisabled.value, [`chip-${props.color}`]: props.color, [`chip-${props.size}`]: props.size, 'chip-clickable': props.clickable || isSelectable.value || attrs.onClick, 'chip-outline': props.variant === 'outline', }, attrs.class, ], 'data-coreui-chip-value': props.value, ...(isDisabled.value && { 'aria-disabled': true }), ...(isSelectable.value && { 'aria-selected': selectedState.value }), ...(isFocusable.value && !attrs.tabindex && { tabindex: 0 }), onClick: handleClick, onKeydown: handleKeydown, ...(props.as === 'button' && { disabled: isDisabled.value }), }, children ) } }, }) export { CChip }