Components update v.3.21.0
parent
18497d55b1
commit
defd6ff6e2
|
@ -1,6 +1,6 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import Accordion from '../accordion/Accordion.vue';
|
||||
import AccordionTab from '../accordiontab/AccordionTab.vue';
|
||||
import Accordion from '@/components/accordion/Accordion.vue';
|
||||
import AccordionTab from '@/components/accordiontab/AccordionTab.vue';
|
||||
|
||||
describe('Accordion.vue', () => {
|
||||
let wrapper;
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { UniqueComponentId, DomHandler } from 'primevue/utils';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler, UniqueComponentId } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'Accordion',
|
||||
|
@ -61,11 +61,11 @@ export default {
|
|||
},
|
||||
expandIcon: {
|
||||
type: String,
|
||||
default: 'pi-chevron-right'
|
||||
default: 'pi pi-chevron-right'
|
||||
},
|
||||
collapseIcon: {
|
||||
type: String,
|
||||
default: 'pi-chevron-down'
|
||||
default: 'pi pi-chevron-down'
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
|
@ -234,7 +234,7 @@ export default {
|
|||
];
|
||||
},
|
||||
getTabHeaderIconClass(i) {
|
||||
return ['p-accordion-toggle-icon pi', this.isTabActive(i) ? this.collapseIcon : this.expandIcon];
|
||||
return ['p-accordion-toggle-icon', this.isTabActive(i) ? this.collapseIcon : this.expandIcon];
|
||||
},
|
||||
getTabContentClass(tab) {
|
||||
return ['p-toggleable-content', this.getTabProp(tab, 'contentClass')];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import AccordionTab from '../accordiontab/AccordionTab.vue';
|
||||
import AccordionTab from '@/components/accordiontab/AccordionTab.vue';
|
||||
|
||||
describe('AccordionTab.vue', () => {
|
||||
it('should exists', () => {
|
||||
|
|
|
@ -76,16 +76,19 @@ export interface PrimeIconsOptions {
|
|||
readonly ARROW_DOWN_RIGHT: string;
|
||||
readonly ARROW_LEFT: string;
|
||||
readonly ARROW_RIGHT: string;
|
||||
readonly ARROW_RIGHT_ARROW_LEFT: string;
|
||||
readonly ARROW_UP: string;
|
||||
readonly ARROW_UP_LEFT: string;
|
||||
readonly ARROW_UP_RIGHT: string;
|
||||
readonly ARROW_H: string;
|
||||
readonly ARROW_V: string;
|
||||
readonly ARROW_A: string;
|
||||
readonly AT: string;
|
||||
readonly BACKWARD: string;
|
||||
readonly BAN: string;
|
||||
readonly BARS: string;
|
||||
readonly BELL: string;
|
||||
readonly BITCOIN: string;
|
||||
readonly BOLT: string;
|
||||
readonly BOOK: string;
|
||||
readonly BOOKMARK: string;
|
||||
|
@ -97,12 +100,14 @@ export interface PrimeIconsOptions {
|
|||
readonly CALENDAR_MINUS: string;
|
||||
readonly CALENDAR_PLUS: string;
|
||||
readonly CALENDAR_TIMES: string;
|
||||
readonly CALCULATOR: string;
|
||||
readonly CAMERA: string;
|
||||
readonly CAR: string;
|
||||
readonly CARET_DOWN: string;
|
||||
readonly CARET_LEFT: string;
|
||||
readonly CARET_RIGHT: string;
|
||||
readonly CARET_UP: string;
|
||||
readonly CART_PLUS: string;
|
||||
readonly CHART_BAR: string;
|
||||
readonly CHART_LINE: string;
|
||||
readonly CHART_PIE: string;
|
||||
|
@ -132,6 +137,7 @@ export interface PrimeIconsOptions {
|
|||
readonly COPY: string;
|
||||
readonly CREDIT_CARD: string;
|
||||
readonly DATABASE: string;
|
||||
readonly DELETELEFT: string;
|
||||
readonly DESKTOP: string;
|
||||
readonly DIRECTIONS: string;
|
||||
readonly DIRECTIONS_ALT: string;
|
||||
|
@ -142,6 +148,7 @@ export interface PrimeIconsOptions {
|
|||
readonly ELLIPSIS_H: string;
|
||||
readonly ELLIPSIS_V: string;
|
||||
readonly ENVELOPE: string;
|
||||
readonly ERASER: string;
|
||||
readonly EURO: string;
|
||||
readonly EXCLAMATION_CIRCLE: string;
|
||||
readonly EXCLAMATION_TRIANGLE: string;
|
||||
|
@ -152,8 +159,12 @@ export interface PrimeIconsOptions {
|
|||
readonly FAST_BACKWARD: string;
|
||||
readonly FAST_FORWARD: string;
|
||||
readonly FILE: string;
|
||||
readonly FILE_EDIT: string;
|
||||
readonly FILE_EXCEL: string;
|
||||
readonly FILE_EXPORT: string;
|
||||
readonly FILE_IMPORT: string;
|
||||
readonly FILE_PDF: string;
|
||||
readonly FILE_WORD: string;
|
||||
readonly FILTER: string;
|
||||
readonly FILTER_FILL: string;
|
||||
readonly FILTER_SLASH: string;
|
||||
|
@ -162,6 +173,7 @@ export interface PrimeIconsOptions {
|
|||
readonly FOLDER: string;
|
||||
readonly FOLDER_OPEN: string;
|
||||
readonly FORWARD: string;
|
||||
readonly GIFT: string;
|
||||
readonly GITHUB: string;
|
||||
readonly GLOBE: string;
|
||||
readonly GOOGLE: string;
|
||||
|
@ -169,6 +181,7 @@ export interface PrimeIconsOptions {
|
|||
readonly HEART: string;
|
||||
readonly HEART_FILL: string;
|
||||
readonly HISTORY: string;
|
||||
readonly HOURGLASS: string;
|
||||
readonly HOME: string;
|
||||
readonly ID_CARD: string;
|
||||
readonly IMAGE: string;
|
||||
|
@ -178,6 +191,7 @@ export interface PrimeIconsOptions {
|
|||
readonly INFO_CIRCLE: string;
|
||||
readonly INSTAGRAM: string;
|
||||
readonly KEY: string;
|
||||
readonly LANGUAGE: string;
|
||||
readonly LINK: string;
|
||||
readonly LINKEDIN: string;
|
||||
readonly LIST: string;
|
||||
|
@ -185,6 +199,8 @@ export interface PrimeIconsOptions {
|
|||
readonly LOCK_OPEN: string;
|
||||
readonly MAP: string;
|
||||
readonly MAP_MARKER: string;
|
||||
readonly MEGAPHONE: string;
|
||||
readonly MICREPHONE: string;
|
||||
readonly MICROSOFT: string;
|
||||
readonly MINUS: string;
|
||||
readonly MINUS_CIRCLE: string;
|
||||
|
@ -253,6 +269,7 @@ export interface PrimeIconsOptions {
|
|||
readonly STEP_FORWARD: string;
|
||||
readonly STEP_FORWARD_ALT: string;
|
||||
readonly STOP: string;
|
||||
readonly STOPWATCH: string;
|
||||
readonly STOP_CIRCLE: string;
|
||||
readonly SUN: string;
|
||||
readonly SYNC: string;
|
||||
|
@ -263,11 +280,14 @@ export interface PrimeIconsOptions {
|
|||
readonly TELEGRAM: string;
|
||||
readonly TH_LARGE: string;
|
||||
readonly THUMBS_DOWN: string;
|
||||
readonly THUMBS_DOWN_FILL: string;
|
||||
readonly THUMBS_UP: string;
|
||||
readonly THUMBS_UP_FILL: string;
|
||||
readonly TICKET: string;
|
||||
readonly TIMES: string;
|
||||
readonly TIMES_CIRCLE: string;
|
||||
readonly TRASH: string;
|
||||
readonly TRUCK: string;
|
||||
readonly TWITTER: string;
|
||||
readonly UNDO: string;
|
||||
readonly UNLOCK: string;
|
||||
|
@ -277,6 +297,7 @@ export interface PrimeIconsOptions {
|
|||
readonly USER_MINUS: string;
|
||||
readonly USER_PLUS: string;
|
||||
readonly USERS: string;
|
||||
readonly VERIFIED: string;
|
||||
readonly VIDEO: string;
|
||||
readonly VIMEO: string;
|
||||
readonly VOLUME_DOWN: string;
|
||||
|
@ -287,6 +308,7 @@ export interface PrimeIconsOptions {
|
|||
readonly WIFI: string;
|
||||
readonly WINDOW_MAXIMIZE: string;
|
||||
readonly WINDOW_MINIMIZE: string;
|
||||
readonly WRENCH: string;
|
||||
readonly YOUTUBE: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,16 +23,19 @@ const PrimeIcons = {
|
|||
ARROW_DOWN_RIGHT: 'pi pi-arrow-down-right',
|
||||
ARROW_LEFT: 'pi pi-arrow-left',
|
||||
ARROW_RIGHT: 'pi pi-arrow-right',
|
||||
ARROW_RIGHT_ARROW_LEFT: 'pi pi-arrow-right-arrow-left',
|
||||
ARROW_UP: 'pi pi-arrow-up',
|
||||
ARROW_UP_LEFT: 'pi pi-arrow-up-left',
|
||||
ARROW_UP_RIGHT: 'pi pi-arrow-up-right',
|
||||
ARROW_H: 'pi pi-arrow-h',
|
||||
ARROW_V: 'pi pi-arrow-v',
|
||||
ARROW_H: 'pi pi-arrows-h',
|
||||
ARROW_V: 'pi pi-arrows-v',
|
||||
ARROW_A: 'pi pi-arrows-alt',
|
||||
AT: 'pi pi-at',
|
||||
BACKWARD: 'pi pi-backward',
|
||||
BAN: 'pi pi-ban',
|
||||
BARS: 'pi pi-bars',
|
||||
BELL: 'pi pi-bell',
|
||||
BITCOIN: 'pi pi-bitcoin',
|
||||
BOLT: 'pi pi-bolt',
|
||||
BOOK: 'pi pi-book',
|
||||
BOOKMARK: 'pi pi-bookmark',
|
||||
|
@ -44,12 +47,14 @@ const PrimeIcons = {
|
|||
CALENDAR_MINUS: 'pi pi-calendar-minus',
|
||||
CALENDAR_PLUS: 'pi pi-calendar-plus',
|
||||
CALENDAR_TIMES: 'pi pi-calendar-times',
|
||||
CALCULATOR: 'pi pi-calculator',
|
||||
CAMERA: 'pi pi-camera',
|
||||
CAR: 'pi pi-car',
|
||||
CARET_DOWN: 'pi pi-caret-down',
|
||||
CARET_LEFT: 'pi pi-caret-left',
|
||||
CARET_RIGHT: 'pi pi-caret-right',
|
||||
CARET_UP: 'pi pi-caret-up',
|
||||
CART_PLUS: 'pi pi-cart-plus',
|
||||
CHART_BAR: 'pi pi-chart-bar',
|
||||
CHART_LINE: 'pi pi-chart-line',
|
||||
CHART_PIE: 'pi pi-chart-pie',
|
||||
|
@ -79,6 +84,7 @@ const PrimeIcons = {
|
|||
COPY: 'pi pi-copy',
|
||||
CREDIT_CARD: 'pi pi-credit-card',
|
||||
DATABASE: 'pi pi-database',
|
||||
DELETELEFT: 'pi pi-delete-left',
|
||||
DESKTOP: 'pi pi-desktop',
|
||||
DIRECTIONS: 'pi pi-directions',
|
||||
DIRECTIONS_ALT: 'pi pi-directions-alt',
|
||||
|
@ -89,6 +95,7 @@ const PrimeIcons = {
|
|||
ELLIPSIS_H: 'pi pi-ellipsis-h',
|
||||
ELLIPSIS_V: 'pi pi-ellipsis-v',
|
||||
ENVELOPE: 'pi pi-envelope',
|
||||
ERASER: 'pi pi-eraser',
|
||||
EURO: 'pi pi-euro',
|
||||
EXCLAMATION_CIRCLE: 'pi pi-exclamation-circle',
|
||||
EXCLAMATION_TRIANGLE: 'pi pi-exclamation-triangle',
|
||||
|
@ -99,8 +106,12 @@ const PrimeIcons = {
|
|||
FAST_BACKWARD: 'pi pi-fast-backward',
|
||||
FAST_FORWARD: 'pi pi-fast-forward',
|
||||
FILE: 'pi pi-file',
|
||||
FILE_EDIT: 'pi pi-file-edit',
|
||||
FILE_EXCEL: 'pi pi-file-excel',
|
||||
FILE_EXPORT: 'pi pi-file-export',
|
||||
FILE_IMPORT: 'pi pi-file-import',
|
||||
FILE_PDF: 'pi pi-file-pdf',
|
||||
FILE_WORD: 'pi pi-file-word',
|
||||
FILTER: 'pi pi-filter',
|
||||
FILTER_FILL: 'pi pi-filter-fill',
|
||||
FILTER_SLASH: 'pi pi-filter-slash',
|
||||
|
@ -109,6 +120,7 @@ const PrimeIcons = {
|
|||
FOLDER: 'pi pi-folder',
|
||||
FOLDER_OPEN: 'pi pi-folder-open',
|
||||
FORWARD: 'pi pi-forward',
|
||||
GIFT: 'pi pi-gift',
|
||||
GITHUB: 'pi pi-github',
|
||||
GLOBE: 'pi pi-globe',
|
||||
GOOGLE: 'pi pi-google',
|
||||
|
@ -116,6 +128,7 @@ const PrimeIcons = {
|
|||
HEART: 'pi pi-heart',
|
||||
HEART_FILL: 'pi pi-heart-fill',
|
||||
HISTORY: 'pi pi-history',
|
||||
HOURGLASS: 'pi pi-hourglass',
|
||||
HOME: 'pi pi-home',
|
||||
ID_CARD: 'pi pi-id-card',
|
||||
IMAGE: 'pi pi-image',
|
||||
|
@ -125,6 +138,7 @@ const PrimeIcons = {
|
|||
INFO_CIRCLE: 'pi pi-info-circle',
|
||||
INSTAGRAM: 'pi pi-instagram',
|
||||
KEY: 'pi pi-key',
|
||||
LANGUAGE: 'pi pi-language',
|
||||
LINK: 'pi pi-link',
|
||||
LINKEDIN: 'pi pi-linkedin',
|
||||
LIST: 'pi pi-list',
|
||||
|
@ -132,6 +146,8 @@ const PrimeIcons = {
|
|||
LOCK_OPEN: 'pi pi-lock-open',
|
||||
MAP: 'pi pi-map',
|
||||
MAP_MARKER: 'pi pi-map-marker',
|
||||
MEGAPHONE: 'pi pi-megaphone',
|
||||
MICREPHONE: 'pi pi-microphone',
|
||||
MICROSOFT: 'pi pi-microsoft',
|
||||
MINUS: 'pi pi-minus',
|
||||
MINUS_CIRCLE: 'pi pi-minus-circle',
|
||||
|
@ -200,6 +216,7 @@ const PrimeIcons = {
|
|||
STEP_FORWARD: 'pi pi-step-forward',
|
||||
STEP_FORWARD_ALT: 'pi pi-step-forward-alt',
|
||||
STOP: 'pi pi-stop',
|
||||
STOPWATCH: 'pi pi-stop-watch',
|
||||
STOP_CIRCLE: 'pi pi-stop-circle',
|
||||
SUN: 'pi pi-sun',
|
||||
SYNC: 'pi pi-sync',
|
||||
|
@ -210,11 +227,14 @@ const PrimeIcons = {
|
|||
TELEGRAM: 'pi pi-telegram',
|
||||
TH_LARGE: 'pi pi-th-large',
|
||||
THUMBS_DOWN: 'pi pi-thumbs-down',
|
||||
THUMBS_DOWN_FILL: 'pi pi-thumbs-down-fill',
|
||||
THUMBS_UP: 'pi pi-thumbs-up',
|
||||
THUMBS_UP_FILL: 'pi pi-thumbs-up-fill',
|
||||
TICKET: 'pi pi-ticket',
|
||||
TIMES: 'pi pi-times',
|
||||
TIMES_CIRCLE: 'pi pi-times-circle',
|
||||
TRASH: 'pi pi-trash',
|
||||
TRUCK: 'pi pi-truck',
|
||||
TWITTER: 'pi pi-twitter',
|
||||
UNDO: 'pi pi-undo',
|
||||
UNLOCK: 'pi pi-unlock',
|
||||
|
@ -224,6 +244,7 @@ const PrimeIcons = {
|
|||
USER_MINUS: 'pi pi-user-minus',
|
||||
USER_PLUS: 'pi pi-user-plus',
|
||||
USERS: 'pi pi-users',
|
||||
VERIFIED: 'pi pi-verified',
|
||||
VIDEO: 'pi pi-video',
|
||||
VIMEO: 'pi pi-vimeo',
|
||||
VOLUME_DOWN: 'pi pi-volume-down',
|
||||
|
@ -234,6 +255,7 @@ const PrimeIcons = {
|
|||
WIFI: 'pi pi-wifi',
|
||||
WINDOW_MAXIMIZE: 'pi pi-window-maximize',
|
||||
WINDOW_MINIMIZE: 'pi pi-window-minimize',
|
||||
WRENCH: 'pi pi-wrench',
|
||||
YOUTUBE: 'pi pi-youtube'
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { HTMLAttributes, InputHTMLAttributes, VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
import { VirtualScrollerProps, VirtualScrollerItemOptions } from '../virtualscroller';
|
||||
import { VirtualScrollerItemOptions, VirtualScrollerProps } from '../virtualscroller';
|
||||
|
||||
type AutoCompleteFieldType = string | ((data: any) => string) | undefined;
|
||||
|
||||
|
@ -187,11 +187,25 @@ export interface AutoCompleteProps {
|
|||
* Uses to pass all properties of the HTMLDivElement to the overlay panel inside the component.
|
||||
*/
|
||||
panelProps?: HTMLAttributes | undefined;
|
||||
/**
|
||||
* Icon to display in the dropdown.
|
||||
* Default value is 'pi pi-chevron-down'.
|
||||
*/
|
||||
dropdownIcon?: string | undefined;
|
||||
/**
|
||||
* Style class of the dropdown button.
|
||||
*/
|
||||
dropdownClass?: string | undefined;
|
||||
/**
|
||||
* Icon to display in loading state.
|
||||
* Default value is 'pi pi-spinner pi-spin'.
|
||||
*/
|
||||
loadingIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in chip remove action.
|
||||
* Default value is 'pi pi-times-circle'.
|
||||
*/
|
||||
removeTokenIcon?: string | undefined;
|
||||
/**
|
||||
* Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it.
|
||||
* @see VirtualScroller.VirtualScrollerProps
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import { nextTick } from 'vue';
|
||||
import AutoComplete from './AutoComplete.vue';
|
||||
|
||||
describe('AutoComplete.vue', () => {
|
||||
|
@ -37,7 +38,7 @@ describe('AutoComplete.vue', () => {
|
|||
expect(wrapper.find('.p-autocomplete-input').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('search copmlete', async () => {
|
||||
it('search complete', async () => {
|
||||
const event = { target: { value: 'b' } };
|
||||
|
||||
wrapper.vm.onInput(event);
|
||||
|
@ -53,4 +54,35 @@ describe('AutoComplete.vue', () => {
|
|||
expect(wrapper.find('.p-autocomplete-items').exists()).toBe(true);
|
||||
expect(wrapper.findAll('.p-autocomplete-item').length).toBe(1);
|
||||
});
|
||||
|
||||
describe('dropdown', () => {
|
||||
it('should have correct custom icon', async () => {
|
||||
wrapper.setProps({
|
||||
dropdown: true,
|
||||
dropdownIcon: 'pi pi-discord'
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
const token = wrapper.find('.p-button-icon');
|
||||
|
||||
expect(token.classes()).toContain('pi-discord');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple', () => {
|
||||
it('should have correct custom icon', async () => {
|
||||
wrapper.setProps({
|
||||
multiple: true,
|
||||
removeTokenIcon: 'pi pi-discord',
|
||||
modelValue: ['foo', 'bar']
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
wrapper.findAll('.p-autocomplete-token-icon').forEach((tokenIcon) => {
|
||||
expect(tokenIcon.classes()).toContain('pi-discord');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<slot name="chip" :value="option">
|
||||
<span class="p-autocomplete-token-label">{{ getOptionLabel(option) }}</span>
|
||||
</slot>
|
||||
<span class="p-autocomplete-token-icon pi pi-times-circle" @click="removeOption($event, i)" aria-hidden="true"></span>
|
||||
<span :class="['p-autocomplete-token-icon', removeTokenIcon]" @click="removeOption($event, i)" aria-hidden="true"></span>
|
||||
</li>
|
||||
<li class="p-autocomplete-input-token" role="option">
|
||||
<input
|
||||
|
@ -84,7 +84,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
<i v-if="searching" :class="loadingIconClass" aria-hidden="true"></i>
|
||||
<Button v-if="dropdown" ref="dropdownButton" type="button" icon="pi pi-chevron-down" class="p-autocomplete-dropdown" tabindex="-1" :disabled="disabled" aria-hidden="true" @click="onDropdownClick" />
|
||||
<Button v-if="dropdown" ref="dropdownButton" type="button" :icon="dropdownIcon" :class="['p-autocomplete-dropdown', dropdownClass]" tabindex="-1" :disabled="disabled" aria-hidden="true" @click="onDropdownClick" />
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ searchResultMessageText }}
|
||||
</span>
|
||||
|
@ -120,15 +120,15 @@
|
|||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="$slots.loader" v-slot:loader="{ options }">
|
||||
<slot name="loader" :options="options"></slot>
|
||||
</template>
|
||||
</VirtualScroller>
|
||||
<slot name="footer" :value="modelValue" :suggestions="visibleOptions"></slot>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
</div>
|
||||
</transition>
|
||||
</Portal>
|
||||
|
@ -136,12 +136,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ConnectedOverlayScrollHandler, UniqueComponentId, ObjectUtils, DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Button from 'primevue/button';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import VirtualScroller from 'primevue/virtualscroller';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Portal from 'primevue/portal';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ObjectUtils, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
|
||||
import VirtualScroller from 'primevue/virtualscroller';
|
||||
|
||||
export default {
|
||||
name: 'AutoComplete',
|
||||
|
@ -242,10 +242,22 @@ export default {
|
|||
type: null,
|
||||
default: null
|
||||
},
|
||||
dropdownIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-chevron-down'
|
||||
},
|
||||
dropdownClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-spinner'
|
||||
},
|
||||
removeTokenIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-times-circle'
|
||||
},
|
||||
virtualScrollerOptions: {
|
||||
type: Object,
|
||||
default: null
|
||||
|
@ -398,7 +410,7 @@ export default {
|
|||
|
||||
this.dirty = true;
|
||||
this.focused = true;
|
||||
this.focusedOptionIndex = this.overlayVisible && this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1;
|
||||
this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.overlayVisible && this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1;
|
||||
this.overlayVisible && this.scrollInView(this.focusedOptionIndex);
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
|
@ -498,7 +510,7 @@ export default {
|
|||
let valid = false;
|
||||
|
||||
if (this.visibleOptions) {
|
||||
const matchedValue = this.visibleOptions.find((option) => this.isOptionMatched(option, event.target.value));
|
||||
const matchedValue = this.visibleOptions.find((option) => this.isOptionMatched(option, this.$refs.focusInput.value || ''));
|
||||
|
||||
if (matchedValue !== undefined) {
|
||||
valid = true;
|
||||
|
@ -651,7 +663,15 @@ export default {
|
|||
this.multiple && event.stopPropagation(); // To prevent onArrowRightKeyOnMultiple method
|
||||
},
|
||||
onHomeKey(event) {
|
||||
event.currentTarget.setSelectionRange(0, 0);
|
||||
const target = event.currentTarget;
|
||||
const len = target.value.length;
|
||||
|
||||
if (event.shiftKey) {
|
||||
event.currentTarget.setSelectionRange(0, len);
|
||||
} else {
|
||||
event.currentTarget.setSelectionRange(0, 0);
|
||||
}
|
||||
|
||||
this.focusedOptionIndex = -1;
|
||||
|
||||
event.preventDefault();
|
||||
|
@ -660,7 +680,12 @@ export default {
|
|||
const target = event.currentTarget;
|
||||
const len = target.value.length;
|
||||
|
||||
target.setSelectionRange(len, len);
|
||||
if (event.shiftKey) {
|
||||
event.currentTarget.setSelectionRange(0, len);
|
||||
} else {
|
||||
target.setSelectionRange(len, len);
|
||||
}
|
||||
|
||||
this.focusedOptionIndex = -1;
|
||||
|
||||
event.preventDefault();
|
||||
|
|
|
@ -30,6 +30,14 @@ export interface AvatarProps {
|
|||
* Default value is 'square'.
|
||||
*/
|
||||
shape?: AvatarShapeType;
|
||||
/**
|
||||
* Establishes a string value that labels the component.
|
||||
*/
|
||||
'aria-label'?: string | undefined;
|
||||
/**
|
||||
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
|
||||
*/
|
||||
'aria-labelledby'?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AvatarSlots {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :class="containerClass">
|
||||
<div :class="containerClass" :aria-labelledby="ariaLabelledby" :aria-label="ariaLabel">
|
||||
<slot>
|
||||
<span v-if="label" class="p-avatar-text">{{ label }}</span>
|
||||
<span v-else-if="icon" :class="iconClass"></span>
|
||||
|
@ -32,6 +32,14 @@ export default {
|
|||
shape: {
|
||||
type: String,
|
||||
default: 'square'
|
||||
},
|
||||
'aria-labelledby': {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
'aria-label': {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import AvatarGroup from './AvatarGroup.vue';
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
import Avatar from '@/components/avatar/Avatar.vue';
|
||||
|
||||
describe('AvatarGroup.vue', () => {
|
||||
it('should exist', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import Badge from './Badge.vue';
|
||||
import Button from '../button/Button.vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
|
||||
describe('Badge.vue', () => {
|
||||
it('should exist', () => {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { config, mount } from '@vue/test-utils';
|
||||
|
||||
import BlockUI from './BlockUI.vue';
|
||||
import Panel from '../panel/Panel.vue';
|
||||
import Button from '../button/Button.vue';
|
||||
import Panel from '@/components/panel/Panel.vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
|
||||
config.global.mocks = {
|
||||
$primevue: {
|
||||
|
@ -10,17 +9,10 @@ config.global.mocks = {
|
|||
zIndex: {
|
||||
modal: 1100
|
||||
}
|
||||
},
|
||||
DomHandler: {
|
||||
addClass: vi.fn(),
|
||||
removeClass: vi.fn()
|
||||
},
|
||||
ZIndexUtils: {
|
||||
set: vi.fn(),
|
||||
clear: vi.fn()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('BlockUI.vue', () => {
|
||||
it('should blocked and unblocked the panel', async () => {
|
||||
const wrapper = mount({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="container" class="p-blockui-container" v-bind="$attrs">
|
||||
<div ref="container" class="p-blockui-container" :aria-busy="isBlocked">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -29,6 +29,11 @@ export default {
|
|||
}
|
||||
},
|
||||
mask: null,
|
||||
data() {
|
||||
return {
|
||||
isBlocked: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
blocked(newValue) {
|
||||
if (newValue === true) this.block();
|
||||
|
@ -61,6 +66,7 @@ export default {
|
|||
ZIndexUtils.set('modal', this.mask, this.baseZIndex + this.$primevue.config.zIndex.modal);
|
||||
}
|
||||
|
||||
this.isBlocked = true;
|
||||
this.$emit('block');
|
||||
},
|
||||
unblock() {
|
||||
|
@ -79,6 +85,7 @@ export default {
|
|||
this.$refs.container.removeChild(this.mask);
|
||||
}
|
||||
|
||||
this.isBlocked = false;
|
||||
this.$emit('unblock');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
import { MenuItem } from '../menuitem';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
export interface BreadcrumbProps {
|
||||
/**
|
||||
|
@ -16,6 +16,14 @@ export interface BreadcrumbProps {
|
|||
* Default value is true.
|
||||
*/
|
||||
exact?: boolean;
|
||||
/**
|
||||
* Defines a string value that labels an interactive element.
|
||||
*/
|
||||
'aria-label'?: string | undefined;
|
||||
/**
|
||||
* Identifier of the underlying menu element.
|
||||
*/
|
||||
'aria-labelledby'?: string | undefined;
|
||||
}
|
||||
|
||||
export interface BreadcrumbSlots {
|
||||
|
|
|
@ -5,7 +5,16 @@ describe('Breadcrumb', () => {
|
|||
it('should exist', () => {
|
||||
const wrapper = mount(Breadcrumb, {
|
||||
global: {
|
||||
stubs: ['router-link']
|
||||
stubs: {
|
||||
'router-link': true
|
||||
},
|
||||
mocks: {
|
||||
$router: {
|
||||
currentRoute: {
|
||||
path: jest.fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
home: { icon: 'pi pi-home', to: '/' },
|
||||
|
@ -14,7 +23,7 @@ describe('Breadcrumb', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.find('.p-breadcrumb.p-component').exists()).toBe(true);
|
||||
expect(wrapper.findAll('.p-breadcrumb-chevron').length).toBe(5);
|
||||
expect(wrapper.findAll('.p-menuitem-separator').length).toBe(5);
|
||||
expect(wrapper.findAll('.p-menuitem-text').length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<template>
|
||||
<nav class="p-breadcrumb p-component" aria-label="Breadcrumb">
|
||||
<ul>
|
||||
<nav class="p-breadcrumb p-component">
|
||||
<ol class="p-breadcrumb-list">
|
||||
<BreadcrumbItem v-if="home" :item="home" class="p-breadcrumb-home" :template="$slots.item" :exact="exact" />
|
||||
<template v-for="item of model" :key="item.label">
|
||||
<li class="p-breadcrumb-chevron pi pi-chevron-right"></li>
|
||||
<li class="p-menuitem-separator">
|
||||
<span class="pi pi-chevron-right" aria-hidden="true"></span>
|
||||
</li>
|
||||
<BreadcrumbItem :item="item" :template="$slots.item" :exact="exact" />
|
||||
</template>
|
||||
</ul>
|
||||
</ol>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
@ -40,7 +42,7 @@ export default {
|
|||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.p-breadcrumb ul {
|
||||
.p-breadcrumb .p-breadcrumb-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
@ -55,6 +57,13 @@ export default {
|
|||
|
||||
.p-breadcrumb .p-menuitem-link {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-breadcrumb .p-menuitem-separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-breadcrumb::-webkit-scrollbar {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<li v-if="visible()" :class="containerClass(item)">
|
||||
<li v-if="visible()" :class="containerClass()">
|
||||
<template v-if="!template">
|
||||
<router-link v-if="item.to" v-slot="{ navigate, href, isActive, isExactActive }" :to="item.to" custom>
|
||||
<a :href="href" :class="linkClass({ isActive, isExactActive })" @click="onClick($event, navigate)">
|
||||
<a :href="href" :class="linkClass({ isActive, isExactActive })" :aria-current="isCurrentUrl()" @click="onClick($event, navigate)">
|
||||
<span v-if="item.icon" :class="iconClass"></span>
|
||||
<span v-if="item.label" class="p-menuitem-text">{{ label() }}</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<a v-else :href="item.url || '#'" :class="linkClass()" @click="onClick" :target="item.target">
|
||||
<a v-else :href="item.url || '#'" :class="linkClass()" :target="item.target" :aria-current="isCurrentUrl()" @click="onClick">
|
||||
<span v-if="item.icon" :class="iconClass"></span>
|
||||
<span v-if="item.label" class="p-menuitem-text">{{ label() }}</span>
|
||||
</a>
|
||||
|
@ -37,8 +37,8 @@ export default {
|
|||
navigate(event);
|
||||
}
|
||||
},
|
||||
containerClass(item) {
|
||||
return [{ 'p-disabled': this.disabled(item) }, this.item.class];
|
||||
containerClass() {
|
||||
return ['p-menuitem', { 'p-disabled': this.disabled() }, this.item.class];
|
||||
},
|
||||
linkClass(routerProps) {
|
||||
return [
|
||||
|
@ -52,11 +52,17 @@ export default {
|
|||
visible() {
|
||||
return typeof this.item.visible === 'function' ? this.item.visible() : this.item.visible !== false;
|
||||
},
|
||||
disabled(item) {
|
||||
return typeof item.disabled === 'function' ? item.disabled() : item.disabled;
|
||||
disabled() {
|
||||
return typeof this.item.disabled === 'function' ? this.item.disabled() : this.item.disabled;
|
||||
},
|
||||
label() {
|
||||
return typeof this.item.label === 'function' ? this.item.label() : this.item.label;
|
||||
},
|
||||
isCurrentUrl() {
|
||||
const { to, url } = this.item;
|
||||
let lastPath = this.$router ? this.$router.currentRoute.path : '';
|
||||
|
||||
return to === lastPath || url === lastPath ? 'page' : undefined;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { h } from 'vue';
|
||||
import Button from '@/components/button/Button.vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Button from '../button/Button.vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
describe('Button.vue', () => {
|
||||
it('is Button element exist', () => {
|
||||
|
|
|
@ -16,10 +16,12 @@ export default {
|
|||
name: 'Button',
|
||||
props: {
|
||||
label: {
|
||||
type: String
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconPos: {
|
||||
type: String,
|
||||
|
@ -30,7 +32,8 @@ export default {
|
|||
default: null
|
||||
},
|
||||
badge: {
|
||||
type: String
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
badgeClass: {
|
||||
type: String,
|
||||
|
|
|
@ -94,6 +94,26 @@ export interface CalendarProps {
|
|||
* Default value is 'pi pi-calendar'.
|
||||
*/
|
||||
icon?: string | undefined;
|
||||
/**
|
||||
* Icon to show in the previous button.
|
||||
* Default value is 'pi pi-chevron-left'.
|
||||
*/
|
||||
previousIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to show in the next button.
|
||||
* Default value is 'pi pi-chevron-right'.
|
||||
*/
|
||||
nextIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to show in each of the increment buttons.
|
||||
* Default value is 'pi pi-chevron-up'.
|
||||
*/
|
||||
incrementIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to show in each of the decrement buttons.
|
||||
* Default value is 'pi pi-chevron-down'.
|
||||
*/
|
||||
decrementIcon?: string | undefined;
|
||||
/**
|
||||
* Number of months to display.
|
||||
* Default value is 1.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Calendar from './Calendar.vue';
|
||||
|
||||
describe('Calendar.vue', () => {
|
||||
|
@ -18,6 +18,7 @@ describe('Calendar.vue', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should exist', async () => {
|
||||
expect(wrapper.find('.p-calendar.p-component').exists()).toBe(true);
|
||||
expect(wrapper.find('.p-inputtext').exists()).toBe(true);
|
||||
|
@ -37,11 +38,12 @@ describe('Calendar.vue', () => {
|
|||
|
||||
const event = { day: 8, month: 2, year: 2022, today: false, selectable: true };
|
||||
|
||||
const onDateSelect = vi.spyOn(wrapper.vm, 'onDateSelect');
|
||||
const onDateSelect = jest.spyOn(wrapper.vm, 'onDateSelect');
|
||||
|
||||
await wrapper.vm.onDateSelect({ currentTarget: { focus: () => {} } }, event);
|
||||
expect(onDateSelect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should calculate the correct view date when in range mode', async () => {
|
||||
const dateOne = new Date();
|
||||
const dateTwo = new Date();
|
||||
|
@ -51,4 +53,28 @@ describe('Calendar.vue', () => {
|
|||
|
||||
expect(wrapper.vm.viewDate).toEqual(dateTwo);
|
||||
});
|
||||
|
||||
it('should respect custom icon settings', async () => {
|
||||
await wrapper.setProps({
|
||||
previousIcon: 'pi pi-discord',
|
||||
nextIcon: 'pi pi-facebook',
|
||||
incrementIcon: 'pi pi-linkedin',
|
||||
decrementIcon: 'pi pi-microsoft',
|
||||
inline: true
|
||||
});
|
||||
|
||||
const previousIcon = wrapper.find('.p-datepicker-prev-icon');
|
||||
const nextIcon = wrapper.find('.p-datepicker-next-icon');
|
||||
|
||||
expect(previousIcon.classes()).toContain('pi-discord');
|
||||
expect(nextIcon.classes()).toContain('pi-facebook');
|
||||
|
||||
await wrapper.setProps({
|
||||
timeOnly: true,
|
||||
hourFormat: '12'
|
||||
});
|
||||
|
||||
expect(wrapper.findAll('.pi-linkedin')).toHaveLength(3);
|
||||
expect(wrapper.findAll('.pi-microsoft')).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
:class="['p-inputtext p-component', inputClass]"
|
||||
:style="inputStyle"
|
||||
:placeholder="placeholder"
|
||||
autocomplete="off"
|
||||
aria-autocomplete="none"
|
||||
aria-haspopup="dialog"
|
||||
:aria-expanded="overlayVisible"
|
||||
|
@ -17,9 +18,10 @@
|
|||
:aria-label="ariaLabel"
|
||||
inputmode="none"
|
||||
:disabled="disabled"
|
||||
:readonly="!manualInput"
|
||||
:readonly="!manualInput || readonly"
|
||||
:tabindex="0"
|
||||
@input="onInput"
|
||||
@click="onInputClick"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@keydown="onKeyDown"
|
||||
|
@ -68,7 +70,7 @@
|
|||
:disabled="disabled"
|
||||
:aria-label="currentView === 'year' ? $primevue.config.locale.prevDecade : currentView === 'month' ? $primevue.config.locale.prevYear : $primevue.config.locale.prevMonth"
|
||||
>
|
||||
<span class="p-datepicker-prev-icon pi pi-chevron-left"></span>
|
||||
<span :class="['p-datepicker-prev-icon', previousIcon]" />
|
||||
</button>
|
||||
<div class="p-datepicker-title">
|
||||
<button
|
||||
|
@ -107,7 +109,7 @@
|
|||
:disabled="disabled"
|
||||
:aria-label="currentView === 'year' ? $primevue.config.locale.nextDecade : currentView === 'month' ? $primevue.config.locale.nextYear : $primevue.config.locale.nextMonth"
|
||||
>
|
||||
<span class="p-datepicker-next-icon pi pi-chevron-right"></span>
|
||||
<span :class="['p-datepicker-next-icon', nextIcon]" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="currentView === 'date'" class="p-datepicker-calendar-container">
|
||||
|
@ -184,7 +186,7 @@
|
|||
@keyup.space="onTimePickerElementMouseUp($event)"
|
||||
type="button"
|
||||
>
|
||||
<span class="pi pi-chevron-up"></span>
|
||||
<span :class="incrementIcon" />
|
||||
</button>
|
||||
<span>{{ formattedCurrentHour }}</span>
|
||||
<button
|
||||
|
@ -201,7 +203,7 @@
|
|||
@keyup.space="onTimePickerElementMouseUp($event)"
|
||||
type="button"
|
||||
>
|
||||
<span class="pi pi-chevron-down"></span>
|
||||
<span :class="decrementIcon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-separator">
|
||||
|
@ -223,7 +225,7 @@
|
|||
@keyup.space="onTimePickerElementMouseUp($event)"
|
||||
type="button"
|
||||
>
|
||||
<span class="pi pi-chevron-up"></span>
|
||||
<span :class="incrementIcon" />
|
||||
</button>
|
||||
<span>{{ formattedCurrentMinute }}</span>
|
||||
<button
|
||||
|
@ -241,7 +243,7 @@
|
|||
@keyup.space="onTimePickerElementMouseUp($event)"
|
||||
type="button"
|
||||
>
|
||||
<span class="pi pi-chevron-down"></span>
|
||||
<span :class="decrementIcon" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showSeconds" class="p-separator">
|
||||
|
@ -263,7 +265,7 @@
|
|||
@keyup.space="onTimePickerElementMouseUp($event)"
|
||||
type="button"
|
||||
>
|
||||
<span class="pi pi-chevron-up"></span>
|
||||
<span :class="incrementIcon" />
|
||||
</button>
|
||||
<span>{{ formattedCurrentSecond }}</span>
|
||||
<button
|
||||
|
@ -281,7 +283,7 @@
|
|||
@keyup.space="onTimePickerElementMouseUp($event)"
|
||||
type="button"
|
||||
>
|
||||
<span class="pi pi-chevron-down"></span>
|
||||
<span :class="decrementIcon" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="hourFormat == '12'" class="p-separator">
|
||||
|
@ -289,11 +291,11 @@
|
|||
</div>
|
||||
<div v-if="hourFormat == '12'" class="p-ampm-picker">
|
||||
<button v-ripple class="p-link" :aria-label="$primevue.config.locale.am" @click="toggleAMPM($event)" type="button" :disabled="disabled">
|
||||
<span class="pi pi-chevron-up"></span>
|
||||
<span :class="incrementIcon" />
|
||||
</button>
|
||||
<span>{{ pm ? 'PM' : 'AM' }}</span>
|
||||
<span>{{ pm ? $primevue.config.locale.pm : $primevue.config.locale.am }}</span>
|
||||
<button v-ripple class="p-link" :aria-label="$primevue.config.locale.pm" @click="toggleAMPM($event)" type="button" :disabled="disabled">
|
||||
<span class="pi pi-chevron-down"></span>
|
||||
<span :class="decrementIcon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -309,11 +311,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Button from 'primevue/button';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Portal from 'primevue/portal';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'Calendar',
|
||||
|
@ -348,6 +350,22 @@ export default {
|
|||
type: String,
|
||||
default: 'pi pi-calendar'
|
||||
},
|
||||
previousIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-chevron-left'
|
||||
},
|
||||
nextIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-chevron-right'
|
||||
},
|
||||
incrementIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-chevron-up'
|
||||
},
|
||||
decrementIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-chevron-down'
|
||||
},
|
||||
numberOfMonths: {
|
||||
type: Number,
|
||||
default: 1
|
||||
|
@ -1094,7 +1112,10 @@ export default {
|
|||
|
||||
if (this.isSingleSelection() && (!this.showTime || this.hideOnDateTimeSelect)) {
|
||||
setTimeout(() => {
|
||||
this.input.focus();
|
||||
if (this.input) {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
this.overlayVisible = false;
|
||||
}, 150);
|
||||
}
|
||||
|
@ -1347,7 +1368,7 @@ export default {
|
|||
}
|
||||
|
||||
if (this.hourFormat === '12') {
|
||||
output += date.getHours() > 11 ? ' PM' : ' AM';
|
||||
output += date.getHours() > 11 ? ` ${this.$primevue.config.locale.pm}` : ` ${this.$primevue.config.locale.am}`;
|
||||
}
|
||||
|
||||
return output;
|
||||
|
@ -1598,6 +1619,10 @@ export default {
|
|||
setTimeout(() => (this.timePickerChange = false), 0);
|
||||
},
|
||||
toggleAMPM(event) {
|
||||
const validHour = this.validateTime(this.currentHour, this.currentMinute, this.currentSecond, !this.pm);
|
||||
|
||||
if (!validHour && (this.maxDate || this.minDate)) return;
|
||||
|
||||
this.pm = !this.pm;
|
||||
this.updateModelTime();
|
||||
event.preventDefault();
|
||||
|
@ -1758,7 +1783,7 @@ export default {
|
|||
throw 'Invalid Time';
|
||||
}
|
||||
|
||||
this.pm = ampm === 'PM' || ampm === 'pm';
|
||||
this.pm = ampm === this.$primevue.config.locale.am || ampm === this.$primevue.config.locale.am.toLowerCase();
|
||||
let time = this.parseTime(timeString);
|
||||
|
||||
value.setHours(time.hour);
|
||||
|
@ -1982,21 +2007,33 @@ export default {
|
|||
const cellContent = event.currentTarget;
|
||||
const cell = cellContent.parentElement;
|
||||
|
||||
const cellIndex = DomHandler.index(cell);
|
||||
|
||||
switch (event.code) {
|
||||
case 'ArrowDown': {
|
||||
cellContent.tabIndex = '-1';
|
||||
let cellIndex = DomHandler.index(cell);
|
||||
|
||||
let nextRow = cell.parentElement.nextElementSibling;
|
||||
|
||||
if (nextRow) {
|
||||
let focusCell = nextRow.children[cellIndex].children[0];
|
||||
let tableRowIndex = DomHandler.index(cell.parentElement);
|
||||
const tableRows = Array.from(cell.parentElement.parentElement.children);
|
||||
const nextTableRows = tableRows.slice(tableRowIndex + 1);
|
||||
|
||||
if (DomHandler.hasClass(focusCell, 'p-disabled')) {
|
||||
let hasNextFocusableDate = nextTableRows.find((el) => {
|
||||
let focusCell = el.children[cellIndex].children[0];
|
||||
|
||||
return !DomHandler.hasClass(focusCell, 'p-disabled');
|
||||
});
|
||||
|
||||
if (hasNextFocusableDate) {
|
||||
let focusCell = hasNextFocusableDate.children[cellIndex].children[0];
|
||||
|
||||
focusCell.tabIndex = '0';
|
||||
focusCell.focus();
|
||||
} else {
|
||||
this.navigationState = { backward: false };
|
||||
this.navForward(event);
|
||||
} else {
|
||||
nextRow.children[cellIndex].children[0].tabIndex = '0';
|
||||
nextRow.children[cellIndex].children[0].focus();
|
||||
}
|
||||
} else {
|
||||
this.navigationState = { backward: false };
|
||||
|
@ -2009,18 +2046,27 @@ export default {
|
|||
|
||||
case 'ArrowUp': {
|
||||
cellContent.tabIndex = '-1';
|
||||
let cellIndex = DomHandler.index(cell);
|
||||
let prevRow = cell.parentElement.previousElementSibling;
|
||||
|
||||
if (prevRow) {
|
||||
let focusCell = prevRow.children[cellIndex].children[0];
|
||||
let tableRowIndex = DomHandler.index(cell.parentElement);
|
||||
const tableRows = Array.from(cell.parentElement.parentElement.children);
|
||||
const prevTableRows = tableRows.slice(0, tableRowIndex).reverse();
|
||||
|
||||
let hasNextFocusableDate = prevTableRows.find((el) => {
|
||||
let focusCell = el.children[cellIndex].children[0];
|
||||
|
||||
return !DomHandler.hasClass(focusCell, 'p-disabled');
|
||||
});
|
||||
|
||||
if (hasNextFocusableDate) {
|
||||
let focusCell = hasNextFocusableDate.children[cellIndex].children[0];
|
||||
|
||||
if (DomHandler.hasClass(focusCell, 'p-disabled')) {
|
||||
this.navigationState = { backward: true };
|
||||
this.navBackward(event);
|
||||
} else {
|
||||
focusCell.tabIndex = '0';
|
||||
focusCell.focus();
|
||||
} else {
|
||||
this.navigationState = { backward: true };
|
||||
this.navBackward(event);
|
||||
}
|
||||
} else {
|
||||
this.navigationState = { backward: true };
|
||||
|
@ -2036,13 +2082,22 @@ export default {
|
|||
let prevCell = cell.previousElementSibling;
|
||||
|
||||
if (prevCell) {
|
||||
let focusCell = prevCell.children[0];
|
||||
const cells = Array.from(cell.parentElement.children);
|
||||
const prevCells = cells.slice(0, cellIndex).reverse();
|
||||
|
||||
let hasNextFocusableDate = prevCells.find((el) => {
|
||||
let focusCell = el.children[0];
|
||||
|
||||
return !DomHandler.hasClass(focusCell, 'p-disabled');
|
||||
});
|
||||
|
||||
if (hasNextFocusableDate) {
|
||||
let focusCell = hasNextFocusableDate.children[0];
|
||||
|
||||
if (DomHandler.hasClass(focusCell, 'p-disabled')) {
|
||||
this.navigateToMonth(event, true, groupIndex);
|
||||
} else {
|
||||
focusCell.tabIndex = '0';
|
||||
focusCell.focus();
|
||||
} else {
|
||||
this.navigateToMonth(event, true, groupIndex);
|
||||
}
|
||||
} else {
|
||||
this.navigateToMonth(event, true, groupIndex);
|
||||
|
@ -2057,13 +2112,21 @@ export default {
|
|||
let nextCell = cell.nextElementSibling;
|
||||
|
||||
if (nextCell) {
|
||||
let focusCell = nextCell.children[0];
|
||||
const cells = Array.from(cell.parentElement.children);
|
||||
const nextCells = cells.slice(cellIndex + 1);
|
||||
let hasNextFocusableDate = nextCells.find((el) => {
|
||||
let focusCell = el.children[0];
|
||||
|
||||
return !DomHandler.hasClass(focusCell, 'p-disabled');
|
||||
});
|
||||
|
||||
if (hasNextFocusableDate) {
|
||||
let focusCell = hasNextFocusableDate.children[0];
|
||||
|
||||
if (DomHandler.hasClass(focusCell, 'p-disabled')) {
|
||||
this.navigateToMonth(event, false, groupIndex);
|
||||
} else {
|
||||
focusCell.tabIndex = '0';
|
||||
focusCell.focus();
|
||||
} else {
|
||||
this.navigateToMonth(event, false, groupIndex);
|
||||
}
|
||||
} else {
|
||||
this.navigateToMonth(event, false, groupIndex);
|
||||
|
@ -2514,6 +2577,11 @@ export default {
|
|||
|
||||
this.$emit('input', event);
|
||||
},
|
||||
onInputClick() {
|
||||
if (this.showOnFocus && this.isEnabled() && !this.overlayVisible) {
|
||||
this.overlayVisible = true;
|
||||
}
|
||||
},
|
||||
onFocus(event) {
|
||||
if (this.showOnFocus && this.isEnabled()) {
|
||||
this.overlayVisible = true;
|
||||
|
@ -2635,7 +2703,7 @@ export default {
|
|||
|
||||
if (propValue && Array.isArray(propValue)) {
|
||||
if (this.isRangeSelection()) {
|
||||
propValue = propValue[1] || propValue[0];
|
||||
propValue = this.inline ? propValue[0] : propValue[1] || propValue[0];
|
||||
} else if (this.isMultipleSelection()) {
|
||||
propValue = propValue[propValue.length - 1];
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VNode } from 'vue';
|
||||
import { ButtonHTMLAttributes, VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
type CarouselOrientationType = 'horizontal' | 'vertical' | undefined;
|
||||
|
@ -85,6 +85,14 @@ export interface CarouselProps {
|
|||
* Default value is true.
|
||||
*/
|
||||
showIndicators?: boolean | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLButtonElement to the previous navigation button.
|
||||
*/
|
||||
prevButtonProps?: ButtonHTMLAttributes | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLButtonElement to the next navigation button.
|
||||
*/
|
||||
nextButtonProps?: ButtonHTMLAttributes | undefined;
|
||||
}
|
||||
|
||||
export interface CarouselSlots {
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Carousel from './Carousel.vue';
|
||||
|
||||
describe('Carousel.vue', () => {
|
||||
it('should exist', async () => {
|
||||
const wrapper = mount(Carousel, {
|
||||
global: {
|
||||
plugins: [PrimeVue]
|
||||
},
|
||||
props: {
|
||||
value: [
|
||||
{
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
<template>
|
||||
<div :id="id" :class="['p-carousel p-component', { 'p-carousel-vertical': isVertical(), 'p-carousel-horizontal': !isVertical() }]">
|
||||
<div :id="id" :class="['p-carousel p-component', { 'p-carousel-vertical': isVertical(), 'p-carousel-horizontal': !isVertical() }]" role="region">
|
||||
<div v-if="$slots.header" class="p-carousel-header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div :class="contentClasses">
|
||||
<div :class="containerClasses">
|
||||
<button v-if="showNavigators" v-ripple :class="['p-carousel-prev p-link', { 'p-disabled': backwardIsDisabled }]" :disabled="backwardIsDisabled" @click="navBackward" type="button">
|
||||
<div :class="containerClasses" :aria-live="allowAutoplay ? 'polite' : 'off'">
|
||||
<button
|
||||
v-if="showNavigators"
|
||||
v-ripple
|
||||
type="button"
|
||||
:class="['p-carousel-prev p-link', { 'p-disabled': backwardIsDisabled }]"
|
||||
:disabled="backwardIsDisabled"
|
||||
:aria-label="ariaPrevButtonLabel"
|
||||
@click="navBackward"
|
||||
v-bind="prevButtonProps"
|
||||
>
|
||||
<span :class="['p-carousel-prev-icon pi', { 'pi-chevron-left': !isVertical(), 'pi-chevron-up': isVertical() }]"></span>
|
||||
</button>
|
||||
|
||||
|
@ -27,6 +36,10 @@
|
|||
v-for="(item, index) of value"
|
||||
:key="index"
|
||||
:class="['p-carousel-item', { 'p-carousel-item-active': firstIndex() <= index && lastIndex() >= index, 'p-carousel-item-start': firstIndex() === index, 'p-carousel-item-end': lastIndex() === index }]"
|
||||
role="group"
|
||||
:aria-hidden="firstIndex() > index || lastIndex() < index ? true : undefined"
|
||||
:aria-label="ariaSlideNumber(index)"
|
||||
:aria-roledescription="ariaSlideLabel"
|
||||
>
|
||||
<slot name="item" :data="item" :index="index"></slot>
|
||||
</div>
|
||||
|
@ -42,13 +55,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button v-if="showNavigators" v-ripple :class="['p-carousel-next p-link', { 'p-disabled': forwardIsDisabled }]" :disabled="forwardIsDisabled" @click="navForward" type="button">
|
||||
<button
|
||||
v-if="showNavigators"
|
||||
v-ripple
|
||||
type="button"
|
||||
:class="['p-carousel-next p-link', { 'p-disabled': forwardIsDisabled }]"
|
||||
:disabled="forwardIsDisabled"
|
||||
:aria-label="ariaNextButtonLabel"
|
||||
@click="navForward"
|
||||
v-bind="nextButtonProps"
|
||||
>
|
||||
<span :class="['p-carousel-prev-icon pi', { 'pi-chevron-right': !isVertical(), 'pi-chevron-down': isVertical() }]"></span>
|
||||
</button>
|
||||
</div>
|
||||
<ul v-if="totalIndicators >= 0 && showIndicators" :class="indicatorsContentClasses">
|
||||
<ul v-if="totalIndicators >= 0 && showIndicators" ref="indicatorContent" :class="indicatorsContentClasses" @keydown="onIndicatorKeydown">
|
||||
<li v-for="(indicator, i) of totalIndicators" :key="'p-carousel-indicator-' + i.toString()" :class="['p-carousel-indicator', { 'p-highlight': d_page === i }]">
|
||||
<button class="p-link" @click="onIndicatorClick($event, i)" type="button" />
|
||||
<button class="p-link" type="button" :tabindex="d_page === i ? '0' : '-1'" :aria-label="ariaPageLabel(i + 1)" :aria-current="d_page === i ? 'page' : undefined" @click="onIndicatorClick($event, i)" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -59,9 +81,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { UniqueComponentId } from 'primevue/utils';
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler, UniqueComponentId } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'Carousel',
|
||||
|
@ -107,8 +128,17 @@ export default {
|
|||
showIndicators: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
prevButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
nextButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
isRemainingItemsAdded: false,
|
||||
data() {
|
||||
return {
|
||||
id: UniqueComponentId(),
|
||||
|
@ -125,7 +155,6 @@ export default {
|
|||
swipeThreshold: 20
|
||||
};
|
||||
},
|
||||
isRemainingItemsAdded: false,
|
||||
watch: {
|
||||
page(newValue) {
|
||||
this.d_page = newValue;
|
||||
|
@ -419,6 +448,84 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
onIndicatorKeydown(event) {
|
||||
switch (event.code) {
|
||||
case 'ArrowRight':
|
||||
this.onRightKey();
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
this.onLeftKey();
|
||||
break;
|
||||
|
||||
case 'Home':
|
||||
this.onHomeKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
this.onEndKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
this.onTabKey();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onRightKey() {
|
||||
const indicators = [...DomHandler.find(this.$refs.indicatorContent, '.p-carousel-indicator')];
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, activeIndex + 1 === indicators.length ? indicators.length - 1 : activeIndex + 1);
|
||||
},
|
||||
onLeftKey() {
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, activeIndex - 1 <= 0 ? 0 : activeIndex - 1);
|
||||
},
|
||||
onHomeKey() {
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, 0);
|
||||
},
|
||||
onEndKey() {
|
||||
const indicators = [...DomHandler.find(this.$refs.indicatorContent, '.p-carousel-indicator')];
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, indicators.length - 1);
|
||||
},
|
||||
onTabKey() {
|
||||
const indicators = [...DomHandler.find(this.$refs.indicatorContent, '.p-carousel-indicator')];
|
||||
const highlightedIndex = indicators.findIndex((ind) => DomHandler.hasClass(ind, 'p-highlight'));
|
||||
|
||||
const activeIndicator = DomHandler.findSingle(this.$refs.indicatorContent, '.p-carousel-indicator > button[tabindex="0"]');
|
||||
const activeIndex = indicators.findIndex((ind) => ind === activeIndicator.parentElement);
|
||||
|
||||
indicators[activeIndex].children[0].tabIndex = '-1';
|
||||
indicators[highlightedIndex].children[0].tabIndex = '0';
|
||||
},
|
||||
findFocusedIndicatorIndex() {
|
||||
const indicators = [...DomHandler.find(this.$refs.indicatorContent, '.p-carousel-indicator')];
|
||||
const activeIndicator = DomHandler.findSingle(this.$refs.indicatorContent, '.p-carousel-indicator > button[tabindex="0"]');
|
||||
|
||||
return indicators.findIndex((ind) => ind === activeIndicator.parentElement);
|
||||
},
|
||||
changedFocusedIndicator(prevInd, nextInd) {
|
||||
const indicators = [...DomHandler.find(this.$refs.indicatorContent, '.p-carousel-indicator')];
|
||||
|
||||
indicators[prevInd].children[0].tabIndex = '-1';
|
||||
indicators[nextInd].children[0].tabIndex = '0';
|
||||
indicators[nextInd].children[0].focus();
|
||||
},
|
||||
bindDocumentListeners() {
|
||||
if (!this.documentResizeListener) {
|
||||
this.documentResizeListener = (e) => {
|
||||
|
@ -507,6 +614,12 @@ export default {
|
|||
},
|
||||
lastIndex() {
|
||||
return this.firstIndex() + this.d_numVisible - 1;
|
||||
},
|
||||
ariaSlideNumber(value) {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.slideNumber.replace(/{slideNumber}/g, value) : undefined;
|
||||
},
|
||||
ariaPageLabel(value) {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.pageLabel.replace(/{page}/g, value) : undefined;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -527,6 +640,15 @@ export default {
|
|||
},
|
||||
indicatorsContentClasses() {
|
||||
return ['p-carousel-indicators p-reset', this.indicatorsContentClass];
|
||||
},
|
||||
ariaSlideLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.slide : undefined;
|
||||
},
|
||||
ariaPrevButtonLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.prevPageLabel : undefined;
|
||||
},
|
||||
ariaNextButtonLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.nextPageLabel : undefined;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
|
|
|
@ -111,11 +111,21 @@ export interface CascadeSelectProps {
|
|||
* Whether the dropdown is in loading state.
|
||||
*/
|
||||
loading?: boolean | undefined;
|
||||
/**
|
||||
* Icon to display in the dropdown.
|
||||
* Default value is 'pi pi-chevron-down'.
|
||||
*/
|
||||
dropdownIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in loading state.
|
||||
* Default value is 'pi pi-spinner pi-spin'.
|
||||
*/
|
||||
loadingIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in the option group.
|
||||
* Default value is 'pi pi-angle-right'.
|
||||
*/
|
||||
optionGroupIcon?: string | undefined;
|
||||
/**
|
||||
* Whether to focus on the first visible or selected element when the overlay panel is shown.
|
||||
* Default value is true.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import CascadeSelect from './CascadeSelect.vue';
|
||||
|
||||
describe('CascadeSelect.vue', () => {
|
||||
|
@ -96,7 +97,7 @@ describe('CascadeSelect.vue', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should exist', async () => {
|
||||
it('should exist', () => {
|
||||
expect(wrapper.find('.p-cascadeselect.p-component').exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -120,4 +121,19 @@ describe('CascadeSelect.vue', () => {
|
|||
expect(sublist.findAll('.p-cascadeselect-item.p-cascadeselect-item-group').length).toBe(2);
|
||||
expect(sublist.findAll('.p-cascadeselect-item-text')[0].text()).toBe('New South Wales');
|
||||
});
|
||||
|
||||
it('should accept custom icons', async () => {
|
||||
wrapper.setProps({
|
||||
dropdownIcon: 'pi pi-discord',
|
||||
optionGroupIcon: 'pi pi-bell'
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find('.p-cascadeselect-trigger-icon').classes()).toContain('pi-discord');
|
||||
|
||||
await wrapper.trigger('click');
|
||||
|
||||
expect(wrapper.find('.p-cascadeselect-group-icon').classes()).toContain('pi-bell');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -54,15 +54,16 @@
|
|||
:optionLabel="optionLabel"
|
||||
:optionValue="optionValue"
|
||||
:optionDisabled="optionDisabled"
|
||||
:optionGroupIcon="optionGroupIcon"
|
||||
:optionGroupLabel="optionGroupLabel"
|
||||
:optionGroupChildren="optionGroupChildren"
|
||||
@option-change="onOptionChange"
|
||||
/>
|
||||
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
</div>
|
||||
</transition>
|
||||
</Portal>
|
||||
|
@ -70,10 +71,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ConnectedOverlayScrollHandler, ObjectUtils, DomHandler, ZIndexUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import CascadeSelectSub from './CascadeSelectSub.vue';
|
||||
import Portal from 'primevue/portal';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ObjectUtils, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
|
||||
import CascadeSelectSub from './CascadeSelectSub.vue';
|
||||
|
||||
export default {
|
||||
name: 'CascadeSelect',
|
||||
|
@ -125,10 +126,18 @@ export default {
|
|||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dropdownIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-chevron-down'
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-spinner pi-spin'
|
||||
},
|
||||
optionGroupIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-angle-right'
|
||||
},
|
||||
autoOptionFocus: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
|
@ -765,7 +774,7 @@ export default {
|
|||
},
|
||||
labelClass() {
|
||||
return [
|
||||
'p-cascadeselect-label',
|
||||
'p-cascadeselect-label p-inputtext',
|
||||
{
|
||||
'p-placeholder': this.label === this.placeholder,
|
||||
'p-cascadeselect-label-empty': !this.$slots['value'] && (this.label === 'p-emptylabel' || this.label.length === 0)
|
||||
|
@ -783,7 +792,7 @@ export default {
|
|||
];
|
||||
},
|
||||
dropdownIconClass() {
|
||||
return ['p-cascadeselect-trigger-icon', this.loading ? this.loadingIcon : 'pi pi-chevron-down'];
|
||||
return ['p-cascadeselect-trigger-icon', this.loading ? this.loadingIcon : this.dropdownIcon];
|
||||
},
|
||||
hasSelectedOption() {
|
||||
return ObjectUtils.isNotEmpty(this.modelValue);
|
||||
|
|
|
@ -3,27 +3,19 @@
|
|||
<template v-for="(processedOption, index) of options" :key="getOptionLabelToRender(processedOption)">
|
||||
<li
|
||||
:id="getOptionId(processedOption)"
|
||||
:class="[
|
||||
'p-cascadeselect-item',
|
||||
{
|
||||
'p-cascadeselect-item-group': isOptionGroup(processedOption),
|
||||
'p-cascadeselect-item-active p-highlight': isOptionActive(processedOption),
|
||||
'p-focus': isOptionFocused(processedOption),
|
||||
'p-disabled': isOptionDisabled(processedOption)
|
||||
}
|
||||
]"
|
||||
:class="getOptionClass(processedOption)"
|
||||
role="treeitem"
|
||||
:aria-label="getOptionLabelToRender(processedOption)"
|
||||
:aria-selected="isOptionGroup(processedOption) ? undefined : isOptionSelected(processedOption)"
|
||||
:aria-expanded="isOptionGroup(processedOption) ? isOptionActive(processedOption) : undefined"
|
||||
:aria-setsize="processedOption.length"
|
||||
:aria-posinset="index + 1"
|
||||
:aria-level="level + 1"
|
||||
:aria-setsize="options.length"
|
||||
:aria-posinset="index + 1"
|
||||
>
|
||||
<div v-ripple class="p-cascadeselect-item-content" @click="onOptionClick($event, processedOption)">
|
||||
<component v-if="templates['option']" :is="templates['option']" :option="processedOption.option" />
|
||||
<span v-else class="p-cascadeselect-item-text">{{ getOptionLabelToRender(processedOption) }}</span>
|
||||
<span v-if="isOptionGroup(processedOption)" class="p-cascadeselect-group-icon pi pi-angle-right" aria-hidden="true"></span>
|
||||
<span v-if="isOptionGroup(processedOption)" :class="['p-cascadeselect-group-icon', optionGroupIcon]" aria-hidden="true"></span>
|
||||
</div>
|
||||
<CascadeSelectSub
|
||||
v-if="isOptionGroup(processedOption) && isOptionActive(processedOption)"
|
||||
|
@ -38,6 +30,7 @@
|
|||
:optionLabel="optionLabel"
|
||||
:optionValue="optionValue"
|
||||
:optionDisabled="optionDisabled"
|
||||
:optionGroupIcon="optionGroupIcon"
|
||||
:optionGroupLabel="optionGroupLabel"
|
||||
:optionGroupChildren="optionGroupChildren"
|
||||
@option-change="onOptionChange"
|
||||
|
@ -48,8 +41,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ObjectUtils, DomHandler } from 'primevue/utils';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler, ObjectUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'CascadeSelectSub',
|
||||
|
@ -61,6 +54,7 @@ export default {
|
|||
optionLabel: String,
|
||||
optionValue: String,
|
||||
optionDisabled: null,
|
||||
optionGroupIcon: String,
|
||||
optionGroupLabel: String,
|
||||
optionGroupChildren: Array,
|
||||
activeOptionPath: Array,
|
||||
|
@ -122,6 +116,17 @@ export default {
|
|||
if (parseInt(containerOffset.left, 10) + itemOuterWidth + sublistWidth > viewport.width - DomHandler.calculateScrollbarWidth()) {
|
||||
this.$el.style.left = '-100%';
|
||||
}
|
||||
},
|
||||
getOptionClass(processedOption) {
|
||||
return [
|
||||
'p-cascadeselect-item',
|
||||
{
|
||||
'p-cascadeselect-item-group': this.isOptionGroup(processedOption),
|
||||
'p-cascadeselect-item-active p-highlight': this.isOptionActive(processedOption),
|
||||
'p-focus': this.isOptionFocused(processedOption),
|
||||
'p-disabled': this.isOptionDisabled(processedOption)
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CanvasHTMLAttributes } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
export interface ChartSelectEvent {
|
||||
|
@ -42,6 +43,10 @@ export interface ChartProps {
|
|||
* Default value is 150.
|
||||
*/
|
||||
height?: number | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the CanvasHTMLAttributes to canvas element inside the component.
|
||||
*/
|
||||
canvasProps?: CanvasHTMLAttributes | undefined;
|
||||
}
|
||||
|
||||
export interface ChartSlots {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="p-chart">
|
||||
<canvas ref="canvas" :width="width" :height="height" @click="onCanvasClick($event)"></canvas>
|
||||
<canvas ref="canvas" :width="width" :height="height" @click="onCanvasClick($event)" v-bind="canvasProps"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -20,6 +20,10 @@ export default {
|
|||
height: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
canvasProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
chart: null,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div v-if="visible" :class="containerClass">
|
||||
<div v-if="visible" :class="containerClass" :aria-label="label">
|
||||
<slot>
|
||||
<img v-if="image" :src="image" />
|
||||
<span v-else-if="icon" :class="iconClass"></span>
|
||||
<div v-if="label" class="p-chip-text">{{ label }}</div>
|
||||
</slot>
|
||||
<span v-if="removable" tabindex="0" :class="removeIconClass" @click="close" @keydown.enter="close"></span>
|
||||
<span v-if="removable" tabindex="0" :class="removeIconClass" @click="close" @keydown="onKeydown"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -41,6 +41,11 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
onKeydown(event) {
|
||||
if (event.key === 'Enter' || event.key === 'Backspace') {
|
||||
this.close(event);
|
||||
}
|
||||
},
|
||||
close(event) {
|
||||
this.visible = false;
|
||||
this.$emit('remove', event);
|
||||
|
|
|
@ -36,9 +36,9 @@ export interface ChipsProps {
|
|||
*/
|
||||
allowDuplicate?: boolean | undefined;
|
||||
/**
|
||||
* Separator char to add an item when pressed in addition to the enter key. Currently only possible value is ','
|
||||
* Separator char to add an item when pressed in addition to the enter key.
|
||||
*/
|
||||
separator?: string | undefined;
|
||||
separator?: string | any;
|
||||
/**
|
||||
* Identifier of the focus input to match a label defined for the chips.
|
||||
*/
|
||||
|
@ -55,6 +55,11 @@ export interface ChipsProps {
|
|||
* Uses to pass all properties of the HTMLInputElement to the focusable input element inside the component.
|
||||
*/
|
||||
inputProps?: InputHTMLAttributes | undefined;
|
||||
/**
|
||||
* Icon to display in chip remove action.
|
||||
* Default value is 'pi pi-times-circle'.
|
||||
*/
|
||||
removeTokenIcon?: string | undefined;
|
||||
/**
|
||||
* When present, it specifies that the element should be disabled.
|
||||
*/
|
||||
|
|
|
@ -19,7 +19,7 @@ describe('Chips.vue', () => {
|
|||
});
|
||||
|
||||
it('should add item', async () => {
|
||||
const addItem = vi.spyOn(wrapper.vm, 'addItem');
|
||||
const addItem = jest.spyOn(wrapper.vm, 'addItem');
|
||||
|
||||
await wrapper.vm.addItem({}, 'PrimeVue', false);
|
||||
|
||||
|
@ -30,4 +30,15 @@ describe('Chips.vue', () => {
|
|||
expect(wrapper.find('.p-chips-token-label').exists()).toBe(true);
|
||||
expect(wrapper.find('.p-chips-token-label').text()).toBe('PrimeVue');
|
||||
});
|
||||
|
||||
it('should have correct custom chip removal icon', async () => {
|
||||
await wrapper.setProps({
|
||||
modelValue: ['foo', 'bar'],
|
||||
removeTokenIcon: 'pi pi-discord'
|
||||
});
|
||||
|
||||
const icon = wrapper.find('.p-chips-token-icon');
|
||||
|
||||
expect(icon.classes()).toContain('pi-discord');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<slot name="chip" :value="val">
|
||||
<span class="p-chips-token-label">{{ val }}</span>
|
||||
</slot>
|
||||
<span class="p-chips-token-icon pi pi-times-circle" @click="removeItem($event, i)" aria-hidden="true"></span>
|
||||
<span :class="['p-chips-token-icon', removeTokenIcon]" @click="removeItem($event, i)" aria-hidden="true"></span>
|
||||
</li>
|
||||
<li class="p-chips-input-token" role="option">
|
||||
<input
|
||||
|
@ -67,7 +67,7 @@ export default {
|
|||
default: null
|
||||
},
|
||||
separator: {
|
||||
type: String,
|
||||
type: [String, Object],
|
||||
default: null
|
||||
},
|
||||
addOnBlur: {
|
||||
|
@ -102,6 +102,10 @@ export default {
|
|||
type: null,
|
||||
default: null
|
||||
},
|
||||
removeTokenIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-times-circle'
|
||||
},
|
||||
'aria-labelledby': {
|
||||
type: String,
|
||||
default: null
|
||||
|
@ -175,7 +179,7 @@ export default {
|
|||
|
||||
default:
|
||||
if (this.separator) {
|
||||
if (this.separator === ',' && event.key === ',') {
|
||||
if (this.separator === event.key || event.key.match(this.separator)) {
|
||||
this.addItem(event, inputValue, true);
|
||||
}
|
||||
}
|
||||
|
@ -252,6 +256,10 @@ export default {
|
|||
this.$refs.input.value = '';
|
||||
this.inputValue = '';
|
||||
|
||||
setTimeout(() => {
|
||||
this.maxedOut && (this.focused = false);
|
||||
}, 0);
|
||||
|
||||
if (preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ColorPicker from './ColorPicker.vue';
|
||||
|
||||
describe('ColorPicker.vue', () => {
|
||||
|
@ -26,7 +26,7 @@ describe('ColorPicker.vue', () => {
|
|||
|
||||
it('should input click triggered', async () => {
|
||||
const input = wrapper.find('.p-colorpicker-preview.p-inputtext');
|
||||
const onInputClick = vi.spyOn(wrapper.vm, 'onInputClick');
|
||||
const onInputClick = jest.spyOn(wrapper.vm, 'onInputClick');
|
||||
|
||||
await input.trigger('click');
|
||||
|
||||
|
@ -41,8 +41,8 @@ describe('ColorPicker.vue', () => {
|
|||
|
||||
await input.trigger('click');
|
||||
|
||||
const onColorMousedown = vi.spyOn(wrapper.vm, 'onColorMousedown');
|
||||
const onHueMousedown = vi.spyOn(wrapper.vm, 'onHueMousedown');
|
||||
const onColorMousedown = jest.spyOn(wrapper.vm, 'onColorMousedown');
|
||||
const onHueMousedown = jest.spyOn(wrapper.vm, 'onHueMousedown');
|
||||
const event = { pageX: 100, pageY: 120, preventDefault: () => {} };
|
||||
const event2 = { pageX: 70, pageY: 20, preventDefault: () => {} };
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Portal from 'primevue/portal';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'ColorPicker',
|
||||
|
@ -411,16 +411,14 @@ export default {
|
|||
this.overlayVisible = !this.overlayVisible;
|
||||
},
|
||||
onInputKeydown(event) {
|
||||
switch (event.which) {
|
||||
//space
|
||||
case 32:
|
||||
switch (event.code) {
|
||||
case 'Space':
|
||||
this.overlayVisible = !this.overlayVisible;
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//escape and tab
|
||||
case 27:
|
||||
case 9:
|
||||
case 'Escape':
|
||||
case 'Tab':
|
||||
this.overlayVisible = false;
|
||||
break;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Vue, { Plugin } from 'vue';
|
||||
import { Plugin } from 'vue';
|
||||
|
||||
interface PrimeVueConfiguration {
|
||||
ripple?: boolean;
|
||||
|
@ -26,6 +26,45 @@ interface PrimeVueLocaleAriaOptions {
|
|||
close?: string;
|
||||
previous?: string;
|
||||
next?: string;
|
||||
navigation?: string;
|
||||
scrollTop?: string;
|
||||
moveUp?: string;
|
||||
moveTop?: string;
|
||||
moveDown?: string;
|
||||
moveBottom?: string;
|
||||
moveToTarget?: string;
|
||||
moveToSource?: string;
|
||||
moveAllToTarget?: string;
|
||||
moveAllToSource?: string;
|
||||
pageLabel?: string;
|
||||
firstPageLabel?: string;
|
||||
lastPageLabel?: string;
|
||||
nextPageLabel?: string;
|
||||
prevPageLabel?: string;
|
||||
rowsPerPageLabel?: string;
|
||||
previousPageLabel?: string;
|
||||
jumpToPageDropdownLabel?: string;
|
||||
jumpToPageInputLabel?: string;
|
||||
selectRow?: string;
|
||||
unselectRow?: string;
|
||||
expandRow?: string;
|
||||
collapseRow?: string;
|
||||
showFilterMenu?: string;
|
||||
hideFilterMenu?: string;
|
||||
filterOperator?: string;
|
||||
filterConstraint?: string;
|
||||
editRow?: string;
|
||||
saveEdit?: string;
|
||||
cancelEdit?: string;
|
||||
listView?: string;
|
||||
gridView?: string;
|
||||
slide?: string;
|
||||
slideNumber?: string;
|
||||
zoomImage?: string;
|
||||
zoomIn?: string;
|
||||
zoomOut?: string;
|
||||
rotateRight?: string;
|
||||
rotateLeft?: string;
|
||||
}
|
||||
|
||||
interface PrimeVueLocaleOptions {
|
||||
|
@ -55,6 +94,8 @@ interface PrimeVueLocaleOptions {
|
|||
choose?: string;
|
||||
upload?: string;
|
||||
cancel?: string;
|
||||
completed?: string;
|
||||
pending?: string;
|
||||
dayNames: string[];
|
||||
dayNamesShort: string[];
|
||||
dayNamesMin: string[];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { reactive, inject } from 'vue';
|
||||
import { FilterMatchMode } from 'primevue/api';
|
||||
import { inject, reactive } from 'vue';
|
||||
|
||||
const defaultOptions = {
|
||||
ripple: false,
|
||||
|
@ -31,6 +31,8 @@ const defaultOptions = {
|
|||
choose: 'Choose',
|
||||
upload: 'Upload',
|
||||
cancel: 'Cancel',
|
||||
completed: 'Completed',
|
||||
pending: 'Pending',
|
||||
dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
||||
dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
dayNamesMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
|
||||
|
@ -77,7 +79,46 @@ const defaultOptions = {
|
|||
unselectAll: 'All items unselected',
|
||||
close: 'Close',
|
||||
previous: 'Previous',
|
||||
next: 'Next'
|
||||
next: 'Next',
|
||||
navigation: 'Navigation',
|
||||
scrollTop: 'Scroll Top',
|
||||
moveTop: 'Move Top',
|
||||
moveUp: 'Move Up',
|
||||
moveDown: 'Move Down',
|
||||
moveBottom: 'Move Bottom',
|
||||
moveToTarget: 'Move to Target',
|
||||
moveToSource: 'Move to Source',
|
||||
moveAllToTarget: 'Move All to Target',
|
||||
moveAllToSource: 'Move All to Source',
|
||||
pageLabel: '{page}',
|
||||
firstPageLabel: 'First Page',
|
||||
lastPageLabel: 'Last Page',
|
||||
nextPageLabel: 'Next Page',
|
||||
prevPageLabel: 'Previous Page',
|
||||
rowsPerPageLabel: 'Rows per page',
|
||||
previousPageLabel: 'Previous Page',
|
||||
jumpToPageDropdownLabel: 'Jump to Page Dropdown',
|
||||
jumpToPageInputLabel: 'Jump to Page Input',
|
||||
selectRow: 'Row Selected',
|
||||
unselectRow: 'Row Unselected',
|
||||
expandRow: 'Row Expanded',
|
||||
collapseRow: 'Row Collapsed',
|
||||
showFilterMenu: 'Show Filter Menu',
|
||||
hideFilterMenu: 'Hide Filter Menu',
|
||||
filterOperator: 'Filter Operator',
|
||||
filterConstraint: 'Filter Constraint',
|
||||
editRow: 'Row Edit',
|
||||
saveEdit: 'Save Edit',
|
||||
cancelEdit: 'Cancel Edit',
|
||||
listView: 'List View',
|
||||
gridView: 'Grid View',
|
||||
slide: 'Slide',
|
||||
slideNumber: '{slideNumber}',
|
||||
zoomImage: 'Zoom Image',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
rotateRight: 'Rotate Right',
|
||||
rotateLeft: 'Rotate Left'
|
||||
}
|
||||
},
|
||||
filterMatchModeOptions: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import ConfirmDialog from './ConfirmDialog.vue';
|
||||
|
||||
|
@ -18,12 +18,13 @@ describe('ConfirmDialog', () => {
|
|||
message: 'Are you sure you want to proceed?',
|
||||
header: 'Confirmation',
|
||||
icon: 'pi pi-exclamation-triangle'
|
||||
}
|
||||
},
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.setData({ visible: true });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('.p-dialog-mask .p-dialog.p-component').exists()).toBe(true);
|
||||
expect(wrapper.find('.p-dialog-title').text()).toBe('Confirmation');
|
||||
|
@ -57,15 +58,15 @@ describe('ConfirmDialog', () => {
|
|||
// eslint-disable-next-line no-console
|
||||
console.log('reject');
|
||||
}
|
||||
}
|
||||
},
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const acceptTriggered = vi.spyOn(wrapper.componentVM.confirmation, 'accept');
|
||||
|
||||
await wrapper.setData({ visible: true });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const acceptTriggered = jest.spyOn(wrapper.componentVM.confirmation, 'accept');
|
||||
const CDAcceptBtn = wrapper.find('.p-confirm-dialog-accept');
|
||||
|
||||
await CDAcceptBtn.trigger('click');
|
||||
|
@ -96,15 +97,15 @@ describe('ConfirmDialog', () => {
|
|||
// eslint-disable-next-line no-console
|
||||
console.log('reject');
|
||||
}
|
||||
}
|
||||
},
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const rejectTriggered = vi.spyOn(wrapper.componentVM.confirmation, 'reject');
|
||||
|
||||
await wrapper.setData({ visible: true });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const rejectTriggered = jest.spyOn(wrapper.componentVM.confirmation, 'reject');
|
||||
const CDRejectBtn = wrapper.find('.p-confirm-dialog-reject');
|
||||
|
||||
await CDRejectBtn.trigger('click');
|
||||
|
@ -127,12 +128,13 @@ describe('ConfirmDialog', () => {
|
|||
message: 'Are you sure you want to proceed?',
|
||||
header: 'Confirmation',
|
||||
icon: 'pi pi-exclamation-triangle'
|
||||
}
|
||||
},
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.setData({ visible: true });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dialogCloseBtn = wrapper.find('.p-dialog-header-close');
|
||||
|
||||
|
@ -158,12 +160,13 @@ describe('ConfirmDialog', () => {
|
|||
header: 'Delete Confirmation',
|
||||
icon: 'pi pi-info-circle',
|
||||
position: 'bottom'
|
||||
}
|
||||
},
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.setData({ visible: true });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('.p-dialog-mask.p-dialog-bottom').exists()).toBe(true);
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<CDialog v-model:visible="visible" :modal="true" :header="header" :blockScroll="blockScroll" :position="position" class="p-confirm-dialog" :breakpoints="breakpoints" :closeOnEscape="closeOnEscape" @update:visible="onHide">
|
||||
<CDialog v-model:visible="visible" role="alertdialog" class="p-confirm-dialog" :modal="true" :header="header" :blockScroll="blockScroll" :position="position" :breakpoints="breakpoints" :closeOnEscape="closeOnEscape" @update:visible="onHide">
|
||||
<template v-if="!$slots.message">
|
||||
<i :class="iconClass" />
|
||||
<span class="p-confirm-dialog-message">{{ message }}</span>
|
||||
|
@ -42,6 +42,11 @@ export default {
|
|||
|
||||
if (options.group === this.group) {
|
||||
this.confirmation = options;
|
||||
|
||||
if (this.confirmation.onShow) {
|
||||
this.confirmation.onShow();
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<Portal>
|
||||
<transition name="p-confirm-popup" @enter="onEnter" @leave="onLeave" @after-leave="onAfterLeave">
|
||||
<div v-if="visible" :ref="containerRef" :class="containerClass" v-bind="$attrs" @click="onOverlayClick">
|
||||
<div v-if="visible" :ref="containerRef" v-focustrap role="alertdialog" :class="containerClass" :aria-modal="visible" @click="onOverlayClick" @keydown="onOverlayKeydown" v-bind="$attrs">
|
||||
<template v-if="!$slots.message">
|
||||
<div class="p-confirm-popup-content">
|
||||
<i :class="iconClass" />
|
||||
|
@ -10,8 +10,8 @@
|
|||
</template>
|
||||
<component v-else :is="$slots.message" :message="confirmation"></component>
|
||||
<div class="p-confirm-popup-footer">
|
||||
<CPButton :label="rejectLabel" :icon="rejectIcon" :class="rejectClass" @click="reject()" />
|
||||
<CPButton :label="acceptLabel" :icon="acceptIcon" :class="acceptClass" @click="accept()" autofocus />
|
||||
<CPButton :label="rejectLabel" :icon="rejectIcon" :class="rejectClass" @click="reject()" @keydown="onRejectKeydown" :autofocus="autoFocusReject" />
|
||||
<CPButton :label="acceptLabel" :icon="acceptIcon" :class="acceptClass" @click="accept()" @keydown="onAcceptKeydown" :autofocus="autoFocusAccept" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -19,11 +19,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmationEventBus from 'primevue/confirmationeventbus';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Button from 'primevue/button';
|
||||
import ConfirmationEventBus from 'primevue/confirmationeventbus';
|
||||
import FocusTrap from 'primevue/focustrap';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Portal from 'primevue/portal';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'ConfirmPopup',
|
||||
|
@ -53,6 +54,11 @@ export default {
|
|||
if (options.group === this.group) {
|
||||
this.confirmation = options;
|
||||
this.target = options.target;
|
||||
|
||||
if (this.confirmation.onShow) {
|
||||
this.confirmation.onShow();
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
};
|
||||
|
@ -101,7 +107,29 @@ export default {
|
|||
|
||||
this.visible = false;
|
||||
},
|
||||
onHide() {
|
||||
if (this.confirmation.onHide) {
|
||||
this.confirmation.onHide();
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
},
|
||||
onAcceptKeydown(event) {
|
||||
if (event.code === 'Space' || event.code === 'Enter') {
|
||||
this.accept();
|
||||
DomHandler.focus(this.target);
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onRejectKeydown(event) {
|
||||
if (event.code === 'Space' || event.code === 'Enter') {
|
||||
this.reject();
|
||||
DomHandler.focus(this.target);
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onEnter(el) {
|
||||
this.focus();
|
||||
this.bindOutsideClickListener();
|
||||
this.bindScrollListener();
|
||||
this.bindResizeListener();
|
||||
|
@ -137,6 +165,10 @@ export default {
|
|||
if (!this.outsideClickListener) {
|
||||
this.outsideClickListener = (event) => {
|
||||
if (this.visible && this.container && !this.container.contains(event.target) && !this.isTargetClicked(event)) {
|
||||
if (this.confirmation.onHide) {
|
||||
this.confirmation.onHide();
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
} else {
|
||||
this.alignOverlay();
|
||||
|
@ -185,6 +217,13 @@ export default {
|
|||
this.resizeListener = null;
|
||||
}
|
||||
},
|
||||
focus() {
|
||||
let focusTarget = this.container.querySelector('[autofocus]');
|
||||
|
||||
if (focusTarget) {
|
||||
focusTarget.focus();
|
||||
}
|
||||
},
|
||||
isTargetClicked(event) {
|
||||
return this.target && (this.target === event.target || this.target.contains(event.target));
|
||||
},
|
||||
|
@ -196,6 +235,12 @@ export default {
|
|||
originalEvent: event,
|
||||
target: this.target
|
||||
});
|
||||
},
|
||||
onOverlayKeydown(event) {
|
||||
if (event.code === 'Escape') {
|
||||
ConfirmationEventBus.emit('close', this.closeListener);
|
||||
DomHandler.focus(this.target);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -231,11 +276,20 @@ export default {
|
|||
},
|
||||
rejectClass() {
|
||||
return ['p-confirm-popup-reject p-button-sm', this.confirmation ? this.confirmation.rejectClass || 'p-button-text' : null];
|
||||
},
|
||||
autoFocusAccept() {
|
||||
return this.confirmation.defaultFocus === undefined || this.confirmation.defaultFocus === 'accept' ? true : false;
|
||||
},
|
||||
autoFocusReject() {
|
||||
return this.confirmation.defaultFocus === 'reject' ? true : false;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CPButton: Button,
|
||||
Portal: Portal
|
||||
},
|
||||
directives: {
|
||||
focustrap: FocusTrap
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
import { MenuItem } from '../menuitem';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
type ContextMenuAppendTo = 'body' | 'self' | string | undefined | HTMLElement;
|
||||
|
||||
|
@ -34,6 +34,18 @@ export interface ContextMenuProps {
|
|||
* Default value is true.
|
||||
*/
|
||||
exact?: boolean | undefined;
|
||||
/**
|
||||
* Index of the element in tabbing order.
|
||||
*/
|
||||
tabindex?: number | string | undefined;
|
||||
/**
|
||||
* Defines a string value that labels an interactive element.
|
||||
*/
|
||||
'aria-label'?: string | undefined;
|
||||
/**
|
||||
* Identifier of the underlying menu element.
|
||||
*/
|
||||
'aria-labelledby'?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ContextMenuSlots {
|
||||
|
@ -49,7 +61,34 @@ export interface ContextMenuSlots {
|
|||
}) => VNode[];
|
||||
}
|
||||
|
||||
export declare type ContextMenuEmits = {};
|
||||
export declare type ContextMenuEmits = {
|
||||
/**
|
||||
* Callback to invoke when the component receives focus.
|
||||
* @param {Event} event - Browser event.
|
||||
*/
|
||||
focus: (event: Event) => void;
|
||||
/**
|
||||
* Callback to invoke when the component loses focus.
|
||||
* @param {Event} event - Browser event.
|
||||
*/
|
||||
blur: (event: Event) => void;
|
||||
/**
|
||||
* Callback to invoke before the popup is shown.
|
||||
*/
|
||||
'before-show': () => void;
|
||||
/**
|
||||
* Callback to invoke before the popup is hidden.
|
||||
*/
|
||||
'before-hide': () => void;
|
||||
/**
|
||||
* Callback to invoke when the popup is shown.
|
||||
*/
|
||||
show: () => void;
|
||||
/**
|
||||
* Callback to invoke when the popup is hidden.
|
||||
*/
|
||||
hide: () => void;
|
||||
};
|
||||
|
||||
declare class ContextMenu extends ClassComponent<ContextMenuProps, ContextMenuSlots, ContextMenuEmits> {
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import ContextMenu from './ContextMenu.vue';
|
||||
|
||||
describe('ContextMenu.vue', () => {
|
||||
|
@ -147,7 +147,7 @@ describe('ContextMenu.vue', () => {
|
|||
|
||||
it('should exist', async () => {
|
||||
const event = { pageX: 100, pageY: 120, preventDefault: () => {}, stopPropagation: () => {} };
|
||||
const show = vi.spyOn(wrapper.vm, 'show');
|
||||
const show = jest.spyOn(wrapper.vm, 'show');
|
||||
|
||||
wrapper.vm.show(event);
|
||||
await wrapper.setData({ visible: true });
|
||||
|
@ -159,7 +159,7 @@ describe('ContextMenu.vue', () => {
|
|||
});
|
||||
|
||||
it('should hide menu', async () => {
|
||||
const hide = vi.spyOn(wrapper.vm, 'hide');
|
||||
const hide = jest.spyOn(wrapper.vm, 'hide');
|
||||
|
||||
await wrapper.setData({ visible: true });
|
||||
|
||||
|
|
|
@ -1,21 +1,46 @@
|
|||
<template>
|
||||
<Portal :appendTo="appendTo">
|
||||
<transition name="p-contextmenu" @enter="onEnter" @leave="onLeave" @after-leave="onAfterLeave">
|
||||
<transition name="p-contextmenu" @enter="onEnter" @after-enter="onAfterEnter" @leave="onLeave" @after-leave="onAfterLeave">
|
||||
<div v-if="visible" :ref="containerRef" :class="containerClass" v-bind="$attrs">
|
||||
<ContextMenuSub :model="model" :root="true" @leaf-click="onLeafClick" :template="$slots.item" :exact="exact" />
|
||||
<ContextMenuSub
|
||||
:ref="listRef"
|
||||
:id="id + '_list'"
|
||||
class="p-contextmenu-root-list"
|
||||
role="menubar"
|
||||
:root="true"
|
||||
:tabindex="tabindex"
|
||||
aria-orientation="vertical"
|
||||
:aria-activedescendant="focused ? focusedItemId : undefined"
|
||||
:menuId="id"
|
||||
:focusedItemId="focused ? focusedItemId : undefined"
|
||||
:items="processedItems"
|
||||
:template="$slots.item"
|
||||
:activeItemPath="activeItemPath"
|
||||
:exact="exact"
|
||||
:aria-labelledby="ariaLabelledby"
|
||||
:aria-label="ariaLabel"
|
||||
:level="0"
|
||||
:visible="submenuVisible"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@keydown="onKeyDown"
|
||||
@item-click="onItemClick"
|
||||
@item-mouseenter="onItemMouseEnter"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</Portal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import ContextMenuSub from './ContextMenuSub.vue';
|
||||
import Portal from 'primevue/portal';
|
||||
import { DomHandler, ObjectUtils, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
|
||||
import ContextMenuSub from './ContextMenuSub.vue';
|
||||
|
||||
export default {
|
||||
name: 'ContextMenu',
|
||||
inheritAttrs: false,
|
||||
emits: ['focus', 'blur', 'show', 'hide'],
|
||||
props: {
|
||||
model: {
|
||||
type: Array,
|
||||
|
@ -40,6 +65,18 @@ export default {
|
|||
exact: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
'aria-labelledby': {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
'aria-label': {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
target: null,
|
||||
|
@ -49,11 +86,29 @@ export default {
|
|||
pageX: null,
|
||||
pageY: null,
|
||||
container: null,
|
||||
list: null,
|
||||
data() {
|
||||
return {
|
||||
visible: false
|
||||
focused: false,
|
||||
focusedItemInfo: { index: -1, level: 0, parentKey: '' },
|
||||
activeItemPath: [],
|
||||
visible: false,
|
||||
submenuVisible: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
activeItemPath(newPath) {
|
||||
if (ObjectUtils.isNotEmpty(newPath)) {
|
||||
this.bindOutsideClickListener();
|
||||
this.bindResizeListener();
|
||||
this.bindDocumentContextMenuListener();
|
||||
} else if (!this.visible) {
|
||||
this.unbindOutsideClickListener();
|
||||
this.unbindResizeListener();
|
||||
this.unbindDocumentContextMenuListener();
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.unbindResizeListener();
|
||||
this.unbindOutsideClickListener();
|
||||
|
@ -63,6 +118,7 @@ export default {
|
|||
ZIndexUtils.clear(this.container);
|
||||
}
|
||||
|
||||
this.target = null;
|
||||
this.container = null;
|
||||
},
|
||||
mounted() {
|
||||
|
@ -71,53 +127,276 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
itemClick(event) {
|
||||
const item = event.item;
|
||||
|
||||
if (item.command) {
|
||||
item.command(event);
|
||||
event.originalEvent.preventDefault();
|
||||
}
|
||||
|
||||
this.hide();
|
||||
getItemProp(item, name) {
|
||||
return item ? ObjectUtils.getItemValue(item[name]) : undefined;
|
||||
},
|
||||
getItemLabel(item) {
|
||||
return this.getItemProp(item, 'label');
|
||||
},
|
||||
isItemDisabled(item) {
|
||||
return this.getItemProp(item, 'disabled');
|
||||
},
|
||||
isItemGroup(item) {
|
||||
return ObjectUtils.isNotEmpty(this.getItemProp(item, 'items'));
|
||||
},
|
||||
isItemSeparator(item) {
|
||||
return this.getItemProp(item, 'separator');
|
||||
},
|
||||
getProccessedItemLabel(processedItem) {
|
||||
return processedItem ? this.getItemLabel(processedItem.item) : undefined;
|
||||
},
|
||||
isProccessedItemGroup(processedItem) {
|
||||
return processedItem && ObjectUtils.isNotEmpty(processedItem.items);
|
||||
},
|
||||
toggle(event) {
|
||||
if (this.visible) this.hide();
|
||||
else this.show(event);
|
||||
},
|
||||
onLeafClick() {
|
||||
this.hide();
|
||||
this.visible ? this.hide() : this.show(event);
|
||||
},
|
||||
show(event) {
|
||||
this.activeItemPath = [];
|
||||
this.focusedItemInfo = { index: -1, level: 0, parentKey: '' };
|
||||
DomHandler.focus(this.list);
|
||||
|
||||
this.pageX = event.pageX;
|
||||
this.pageY = event.pageY;
|
||||
|
||||
if (this.visible) this.position();
|
||||
else this.visible = true;
|
||||
this.visible ? this.position() : (this.visible = true);
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
this.activeItemPath = [];
|
||||
this.focusedItemInfo = { index: -1, level: 0, parentKey: '' };
|
||||
},
|
||||
onFocus(event) {
|
||||
this.focused = true;
|
||||
this.focusedItemInfo = this.focusedItemInfo.index !== -1 ? this.focusedItemInfo : { index: -1, level: 0, parentKey: '' };
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
onBlur(event) {
|
||||
this.focused = false;
|
||||
this.focusedItemInfo = { index: -1, level: 0, parentKey: '' };
|
||||
this.searchValue = '';
|
||||
this.$emit('blur', event);
|
||||
},
|
||||
onKeyDown(event) {
|
||||
const metaKey = event.metaKey || event.ctrlKey;
|
||||
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
this.onArrowDownKey(event);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
this.onArrowUpKey(event);
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
this.onArrowLeftKey(event);
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
this.onArrowRightKey(event);
|
||||
break;
|
||||
|
||||
case 'Home':
|
||||
this.onHomeKey(event);
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
this.onEndKey(event);
|
||||
break;
|
||||
|
||||
case 'Space':
|
||||
this.onSpaceKey(event);
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
this.onEnterKey(event);
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
this.onEscapeKey(event);
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
this.onTabKey(event);
|
||||
break;
|
||||
|
||||
case 'PageDown':
|
||||
case 'PageUp':
|
||||
case 'Backspace':
|
||||
case 'ShiftLeft':
|
||||
case 'ShiftRight':
|
||||
//NOOP
|
||||
break;
|
||||
|
||||
default:
|
||||
if (!metaKey && ObjectUtils.isPrintableCharacter(event.key)) {
|
||||
this.searchItems(event, event.key);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
onItemChange(event) {
|
||||
const { processedItem, isFocus } = event;
|
||||
|
||||
if (ObjectUtils.isEmpty(processedItem)) return;
|
||||
|
||||
const { index, key, level, parentKey, items } = processedItem;
|
||||
const grouped = ObjectUtils.isNotEmpty(items);
|
||||
const activeItemPath = this.activeItemPath.filter((p) => p.parentKey !== parentKey && p.parentKey !== key);
|
||||
|
||||
if (grouped) {
|
||||
activeItemPath.push(processedItem);
|
||||
this.submenuVisible = true;
|
||||
}
|
||||
|
||||
this.focusedItemInfo = { index, level, parentKey };
|
||||
this.activeItemPath = activeItemPath;
|
||||
|
||||
isFocus && DomHandler.focus(this.list);
|
||||
},
|
||||
onItemClick(event) {
|
||||
const { processedItem } = event;
|
||||
const grouped = this.isProccessedItemGroup(processedItem);
|
||||
const selected = this.isSelected(processedItem);
|
||||
|
||||
if (selected) {
|
||||
const { index, key, level, parentKey } = processedItem;
|
||||
|
||||
this.activeItemPath = this.activeItemPath.filter((p) => key !== p.key && key.startsWith(p.key));
|
||||
this.focusedItemInfo = { index, level, parentKey };
|
||||
|
||||
DomHandler.focus(this.list);
|
||||
} else {
|
||||
grouped ? this.onItemChange(event) : this.hide();
|
||||
}
|
||||
},
|
||||
onItemMouseEnter(event) {
|
||||
this.onItemChange(event);
|
||||
},
|
||||
onArrowDownKey(event) {
|
||||
const itemIndex = this.focusedItemInfo.index !== -1 ? this.findNextItemIndex(this.focusedItemInfo.index) : this.findFirstFocusedItemIndex();
|
||||
|
||||
this.changeFocusedItemIndex(event, itemIndex);
|
||||
event.preventDefault();
|
||||
},
|
||||
onArrowUpKey(event) {
|
||||
if (event.altKey) {
|
||||
if (this.focusedItemInfo.index !== -1) {
|
||||
const processedItem = this.visibleItems[this.focusedItemInfo.index];
|
||||
const grouped = this.isProccessedItemGroup(processedItem);
|
||||
|
||||
!grouped && this.onItemChange({ originalEvent: event, processedItem });
|
||||
}
|
||||
|
||||
this.popup && this.hide();
|
||||
event.preventDefault();
|
||||
} else {
|
||||
const itemIndex = this.focusedItemInfo.index !== -1 ? this.findPrevItemIndex(this.focusedItemInfo.index) : this.findLastFocusedItemIndex();
|
||||
|
||||
this.changeFocusedItemIndex(event, itemIndex);
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onArrowLeftKey(event) {
|
||||
const processedItem = this.visibleItems[this.focusedItemInfo.index];
|
||||
const parentItem = this.activeItemPath.find((p) => p.key === processedItem.parentKey);
|
||||
const root = ObjectUtils.isEmpty(processedItem.parent);
|
||||
|
||||
if (!root) {
|
||||
this.focusedItemInfo = { index: -1, parentKey: parentItem ? parentItem.parentKey : '' };
|
||||
this.searchValue = '';
|
||||
this.onArrowDownKey(event);
|
||||
}
|
||||
|
||||
this.activeItemPath = this.activeItemPath.filter((p) => p.parentKey !== this.focusedItemInfo.parentKey);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onArrowRightKey(event) {
|
||||
const processedItem = this.visibleItems[this.focusedItemInfo.index];
|
||||
const grouped = this.isProccessedItemGroup(processedItem);
|
||||
|
||||
if (grouped) {
|
||||
this.onItemChange({ originalEvent: event, processedItem });
|
||||
this.focusedItemInfo = { index: -1, parentKey: processedItem.key };
|
||||
this.searchValue = '';
|
||||
this.onArrowDownKey(event);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onHomeKey(event) {
|
||||
this.changeFocusedItemIndex(event, this.findFirstItemIndex());
|
||||
event.preventDefault();
|
||||
},
|
||||
onEndKey(event) {
|
||||
this.changeFocusedItemIndex(event, this.findLastItemIndex());
|
||||
event.preventDefault();
|
||||
},
|
||||
onEnterKey(event) {
|
||||
if (this.focusedItemInfo.index !== -1) {
|
||||
const element = DomHandler.findSingle(this.list, `li[id="${`${this.focusedItemId}`}"]`);
|
||||
const anchorElement = element && DomHandler.findSingle(element, '.p-menuitem-link');
|
||||
|
||||
anchorElement ? anchorElement.click() : element && element.click();
|
||||
const processedItem = this.visibleItems[this.focusedItemInfo.index];
|
||||
const grouped = this.isProccessedItemGroup(processedItem);
|
||||
|
||||
!grouped && (this.focusedItemInfo.index = this.findFirstFocusedItemIndex());
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onSpaceKey(event) {
|
||||
this.onEnterKey(event);
|
||||
},
|
||||
onEscapeKey(event) {
|
||||
this.hide();
|
||||
!this.popup && (this.focusedItemInfo.index = this.findFirstFocusedItemIndex());
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onTabKey(event) {
|
||||
if (this.focusedItemInfo.index !== -1) {
|
||||
const processedItem = this.visibleItems[this.focusedItemInfo.index];
|
||||
const grouped = this.isProccessedItemGroup(processedItem);
|
||||
|
||||
!grouped && this.onItemChange({ originalEvent: event, processedItem });
|
||||
}
|
||||
|
||||
this.hide();
|
||||
},
|
||||
onEnter(el) {
|
||||
this.position();
|
||||
this.bindOutsideClickListener();
|
||||
this.bindResizeListener();
|
||||
|
||||
if (this.autoZIndex) {
|
||||
ZIndexUtils.set('menu', el, this.baseZIndex + this.$primevue.config.zIndex.menu);
|
||||
}
|
||||
},
|
||||
onAfterEnter() {
|
||||
this.bindOutsideClickListener();
|
||||
this.bindResizeListener();
|
||||
this.bindDocumentContextMenuListener();
|
||||
|
||||
this.$emit('show');
|
||||
DomHandler.focus(this.list);
|
||||
},
|
||||
onLeave() {
|
||||
this.unbindOutsideClickListener();
|
||||
this.unbindResizeListener();
|
||||
this.$emit('hide');
|
||||
this.container = null;
|
||||
},
|
||||
onAfterLeave(el) {
|
||||
if (this.autoZIndex) {
|
||||
ZIndexUtils.clear(el);
|
||||
}
|
||||
|
||||
this.unbindOutsideClickListener();
|
||||
this.unbindResizeListener();
|
||||
this.unbindDocumentContextMenuListener();
|
||||
},
|
||||
position() {
|
||||
let left = this.pageX + 1;
|
||||
|
@ -152,7 +431,10 @@ export default {
|
|||
bindOutsideClickListener() {
|
||||
if (!this.outsideClickListener) {
|
||||
this.outsideClickListener = (event) => {
|
||||
if (this.visible && this.container && !this.container.contains(event.target) && !event.ctrlKey) {
|
||||
const isOutsideContainer = this.container && !this.container.contains(event.target);
|
||||
const isOutsideTarget = this.visible ? !(this.target && (this.target === event.target || this.target.contains(event.target))) : true;
|
||||
|
||||
if (isOutsideContainer && isOutsideTarget) {
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
|
@ -186,7 +468,7 @@ export default {
|
|||
bindDocumentContextMenuListener() {
|
||||
if (!this.documentContextMenuListener) {
|
||||
this.documentContextMenuListener = (event) => {
|
||||
this.show(event);
|
||||
event.button !== 2 ? this.show(event) : this.hide();
|
||||
};
|
||||
|
||||
document.addEventListener('contextmenu', this.documentContextMenuListener);
|
||||
|
@ -198,19 +480,142 @@ export default {
|
|||
this.documentContextMenuListener = null;
|
||||
}
|
||||
},
|
||||
isItemMatched(processedItem) {
|
||||
return this.isValidItem(processedItem) && this.getProccessedItemLabel(processedItem).toLocaleLowerCase().startsWith(this.searchValue.toLocaleLowerCase());
|
||||
},
|
||||
isValidItem(processedItem) {
|
||||
return !!processedItem && !this.isItemDisabled(processedItem.item) && !this.isItemSeparator(processedItem.item);
|
||||
},
|
||||
isValidSelectedItem(processedItem) {
|
||||
return this.isValidItem(processedItem) && this.isSelected(processedItem);
|
||||
},
|
||||
isSelected(processedItem) {
|
||||
return this.activeItemPath.some((p) => p.key === processedItem.key);
|
||||
},
|
||||
findFirstItemIndex() {
|
||||
return this.visibleItems.findIndex((processedItem) => this.isValidItem(processedItem));
|
||||
},
|
||||
findLastItemIndex() {
|
||||
return ObjectUtils.findLastIndex(this.visibleItems, (processedItem) => this.isValidItem(processedItem));
|
||||
},
|
||||
findNextItemIndex(index) {
|
||||
const matchedItemIndex = index < this.visibleItems.length - 1 ? this.visibleItems.slice(index + 1).findIndex((processedItem) => this.isValidItem(processedItem)) : -1;
|
||||
|
||||
return matchedItemIndex > -1 ? matchedItemIndex + index + 1 : index;
|
||||
},
|
||||
findPrevItemIndex(index) {
|
||||
const matchedItemIndex = index > 0 ? ObjectUtils.findLastIndex(this.visibleItems.slice(0, index), (processedItem) => this.isValidItem(processedItem)) : -1;
|
||||
|
||||
return matchedItemIndex > -1 ? matchedItemIndex : index;
|
||||
},
|
||||
findSelectedItemIndex() {
|
||||
return this.visibleItems.findIndex((processedItem) => this.isValidSelectedItem(processedItem));
|
||||
},
|
||||
findFirstFocusedItemIndex() {
|
||||
const selectedIndex = this.findSelectedItemIndex();
|
||||
|
||||
return selectedIndex < 0 ? this.findFirstItemIndex() : selectedIndex;
|
||||
},
|
||||
findLastFocusedItemIndex() {
|
||||
const selectedIndex = this.findSelectedItemIndex();
|
||||
|
||||
return selectedIndex < 0 ? this.findLastItemIndex() : selectedIndex;
|
||||
},
|
||||
searchItems(event, char) {
|
||||
this.searchValue = (this.searchValue || '') + char;
|
||||
|
||||
let itemIndex = -1;
|
||||
let matched = false;
|
||||
|
||||
if (this.focusedItemInfo.index !== -1) {
|
||||
itemIndex = this.visibleItems.slice(this.focusedItemInfo.index).findIndex((processedItem) => this.isItemMatched(processedItem));
|
||||
itemIndex = itemIndex === -1 ? this.visibleItems.slice(0, this.focusedItemInfo.index).findIndex((processedItem) => this.isItemMatched(processedItem)) : itemIndex + this.focusedItemInfo.index;
|
||||
} else {
|
||||
itemIndex = this.visibleItems.findIndex((processedItem) => this.isItemMatched(processedItem));
|
||||
}
|
||||
|
||||
if (itemIndex !== -1) {
|
||||
matched = true;
|
||||
}
|
||||
|
||||
if (itemIndex === -1 && this.focusedItemInfo.index === -1) {
|
||||
itemIndex = this.findFirstFocusedItemIndex();
|
||||
}
|
||||
|
||||
if (itemIndex !== -1) {
|
||||
this.changeFocusedItemIndex(event, itemIndex);
|
||||
}
|
||||
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.searchValue = '';
|
||||
this.searchTimeout = null;
|
||||
}, 500);
|
||||
|
||||
return matched;
|
||||
},
|
||||
changeFocusedItemIndex(event, index) {
|
||||
if (this.focusedItemInfo.index !== index) {
|
||||
this.focusedItemInfo.index = index;
|
||||
this.scrollInView();
|
||||
}
|
||||
},
|
||||
scrollInView(index = -1) {
|
||||
const id = index !== -1 ? `${this.id}_${index}` : this.focusedItemId;
|
||||
const element = DomHandler.findSingle(this.list, `li[id="${id}"]`);
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' });
|
||||
}
|
||||
},
|
||||
createProcessedItems(items, level = 0, parent = {}, parentKey = '') {
|
||||
const processedItems = [];
|
||||
|
||||
items &&
|
||||
items.forEach((item, index) => {
|
||||
const key = (parentKey !== '' ? parentKey + '_' : '') + index;
|
||||
const newItem = {
|
||||
item,
|
||||
index,
|
||||
level,
|
||||
key,
|
||||
parent,
|
||||
parentKey
|
||||
};
|
||||
|
||||
newItem['items'] = this.createProcessedItems(item.items, level + 1, newItem, key);
|
||||
processedItems.push(newItem);
|
||||
});
|
||||
|
||||
return processedItems;
|
||||
},
|
||||
containerRef(el) {
|
||||
this.container = el;
|
||||
},
|
||||
listRef(el) {
|
||||
this.list = el ? el.$el : undefined;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerClass() {
|
||||
return [
|
||||
'p-contextmenu p-component',
|
||||
{
|
||||
'p-input-filled': this.$primevue.config.inputStyle === 'filled',
|
||||
'p-ripple-disabled': this.$primevue.config.ripple === false
|
||||
}
|
||||
];
|
||||
return ['p-contextmenu p-component', { 'p-input-filled': this.$primevue.config.inputStyle === 'filled', 'p-ripple-disabled': this.$primevue.config.ripple === false }];
|
||||
},
|
||||
processedItems() {
|
||||
return this.createProcessedItems(this.model || []);
|
||||
},
|
||||
visibleItems() {
|
||||
const processedItem = this.activeItemPath.find((p) => p.key === this.focusedItemInfo.parentKey);
|
||||
|
||||
return processedItem ? processedItem.items : this.processedItems;
|
||||
},
|
||||
id() {
|
||||
return this.$attrs.id || UniqueComponentId();
|
||||
},
|
||||
focusedItemId() {
|
||||
return this.focusedItemInfo.index !== -1 ? `${this.id}${ObjectUtils.isNotEmpty(this.focusedItemInfo.parentKey) ? '_' + this.focusedItemInfo.parentKey : ''}_${this.focusedItemInfo.index}` : null;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -1,61 +1,92 @@
|
|||
<template>
|
||||
<transition name="p-contextmenusub" @enter="onEnter">
|
||||
<ul v-if="root ? true : parentActive" ref="container" :class="containerClass" role="menu">
|
||||
<template v-for="(item, i) of model" :key="label(item) + i.toString()">
|
||||
<li v-if="visible(item) && !item.separator" role="none" :class="getItemClass(item)" :style="item.style" @mouseenter="onItemMouseEnter($event, item)">
|
||||
<template v-if="!template">
|
||||
<router-link v-if="item.to && !disabled(item)" v-slot="{ navigate, href, isActive, isExactActive }" :to="item.to" custom>
|
||||
<a v-ripple :href="href" @click="onItemClick($event, item, navigate)" :class="linkClass(item, { isActive, isExactActive })" role="menuitem">
|
||||
<span v-if="item.icon" :class="['p-menuitem-icon', item.icon]"></span>
|
||||
<span class="p-menuitem-text">{{ label(item) }}</span>
|
||||
<ul v-if="root ? true : visible" ref="container">
|
||||
<template v-for="(processedItem, index) of items" :key="getItemKey(processedItem)">
|
||||
<li
|
||||
v-if="isItemVisible(processedItem) && !getItemProp(processedItem, 'separator')"
|
||||
:id="getItemId(processedItem)"
|
||||
:style="getItemProp(processedItem, 'style')"
|
||||
:class="getItemClass(processedItem)"
|
||||
role="menuitem"
|
||||
:aria-label="getItemLabel(processedItem)"
|
||||
:aria-disabled="isItemDisabled(processedItem) || undefined"
|
||||
:aria-expanded="isItemGroup(processedItem) ? isItemActive(processedItem) : undefined"
|
||||
:aria-haspopup="isItemGroup(processedItem) && !getItemProp(processedItem, 'to') ? 'menu' : undefined"
|
||||
:aria-level="level + 1"
|
||||
:aria-setsize="getAriaSetSize()"
|
||||
:aria-posinset="getAriaPosInset(index)"
|
||||
>
|
||||
<div class="p-menuitem-content" @click="onItemClick($event, processedItem)" @mouseenter="onItemMouseEnter($event, processedItem)">
|
||||
<template v-if="!template">
|
||||
<router-link v-if="getItemProp(processedItem, 'to') && !isItemDisabled(processedItem)" v-slot="{ navigate, href, isActive, isExactActive }" :to="getItemProp(processedItem, 'to')" custom>
|
||||
<a v-ripple :href="href" :class="getItemActionClass(processedItem, { isActive, isExactActive })" tabindex="-1" aria-hidden="true" @click="onItemActionClick($event, navigate)">
|
||||
<span v-if="getItemProp(processedItem, 'icon')" :class="getItemIconClass(processedItem)"></span>
|
||||
<span class="p-menuitem-text">{{ getItemLabel(processedItem) }}</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<a v-else v-ripple :href="getItemProp(processedItem, 'url')" :class="getItemActionClass(processedItem)" :target="getItemProp(processedItem, 'target')" tabindex="-1" aria-hidden="true">
|
||||
<span v-if="getItemProp(processedItem, 'icon')" :class="getItemIconClass(processedItem)"></span>
|
||||
<span class="p-menuitem-text">{{ getItemLabel(processedItem) }}</span>
|
||||
<span v-if="getItemProp(processedItem, 'items')" class="p-submenu-icon pi pi-angle-right"></span>
|
||||
</a>
|
||||
</router-link>
|
||||
<a
|
||||
v-else
|
||||
v-ripple
|
||||
:href="item.url"
|
||||
:class="linkClass(item)"
|
||||
:target="item.target"
|
||||
@click="onItemClick($event, item)"
|
||||
:aria-haspopup="item.items != null"
|
||||
:aria-expanded="item === activeItem"
|
||||
role="menuitem"
|
||||
:tabindex="disabled(item) ? null : '0'"
|
||||
>
|
||||
<span v-if="item.icon" :class="['p-menuitem-icon', item.icon]"></span>
|
||||
<span class="p-menuitem-text">{{ label(item) }}</span>
|
||||
<span v-if="item.items" class="p-submenu-icon pi pi-angle-right"></span>
|
||||
</a>
|
||||
</template>
|
||||
<component v-else :is="template" :item="item"></component>
|
||||
<ContextMenuSub v-if="visible(item) && item.items" :key="label(item) + '_sub_'" :model="item.items" :template="template" @leaf-click="onLeafClick" :parentActive="item === activeItem" :exact="exact" />
|
||||
</template>
|
||||
<component v-else :is="template" :item="processedItem.item"></component>
|
||||
</div>
|
||||
<ContextMenuSub
|
||||
v-if="isItemVisible(processedItem) && isItemGroup(processedItem)"
|
||||
:id="getItemId(processedItem) + '_list'"
|
||||
role="menu"
|
||||
class="p-submenu-list"
|
||||
:menuId="menuId"
|
||||
:focusedItemId="focusedItemId"
|
||||
:items="processedItem.items"
|
||||
:template="template"
|
||||
:activeItemPath="activeItemPath"
|
||||
:exact="exact"
|
||||
:level="level + 1"
|
||||
:visible="isItemActive(processedItem) && isItemGroup(processedItem)"
|
||||
@item-click="$emit('item-click', $event)"
|
||||
@item-mouseenter="$emit('item-mouseenter', $event)"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="visible(item) && item.separator" :key="'separator' + i.toString()" :class="['p-menu-separator', item.class]" :style="item.style" role="separator"></li>
|
||||
<li v-if="isItemVisible(processedItem) && getItemProp(processedItem, 'separator')" :id="getItemId(processedItem)" :style="getItemProp(processedItem, 'style')" :class="getSeparatorItemClass(processedItem)" role="separator"></li>
|
||||
</template>
|
||||
</ul>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler, ObjectUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'ContextMenuSub',
|
||||
emits: ['leaf-click'],
|
||||
emits: ['item-click', 'item-mouseenter'],
|
||||
props: {
|
||||
model: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
menuId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
focusedItemId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
root: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
parentActive: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
template: {
|
||||
type: Function,
|
||||
default: null
|
||||
|
@ -63,60 +94,57 @@ export default {
|
|||
exact: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeItem: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
parentActive(newValue) {
|
||||
if (!newValue) {
|
||||
this.activeItem = null;
|
||||
}
|
||||
},
|
||||
activeItemPath: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onItemMouseEnter(event, item) {
|
||||
if (this.disabled(item)) {
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeItem = item;
|
||||
getItemId(processedItem) {
|
||||
return `${this.menuId}_${processedItem.key}`;
|
||||
},
|
||||
onItemClick(event, item, navigate) {
|
||||
if (this.disabled(item)) {
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.command) {
|
||||
item.command({
|
||||
originalEvent: event,
|
||||
item: item
|
||||
});
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
if (this.activeItem && item === this.activeItem) this.activeItem = null;
|
||||
else this.activeItem = item;
|
||||
}
|
||||
|
||||
if (!item.items) {
|
||||
this.onLeafClick();
|
||||
}
|
||||
|
||||
if (item.to && navigate) {
|
||||
navigate(event);
|
||||
}
|
||||
getItemKey(processedItem) {
|
||||
return this.getItemId(processedItem);
|
||||
},
|
||||
onLeafClick() {
|
||||
this.activeItem = null;
|
||||
this.$emit('leaf-click');
|
||||
getItemProp(processedItem, name) {
|
||||
return processedItem && processedItem.item ? ObjectUtils.getItemValue(processedItem.item[name]) : undefined;
|
||||
},
|
||||
getItemLabel(processedItem) {
|
||||
return this.getItemProp(processedItem, 'label');
|
||||
},
|
||||
isItemActive(processedItem) {
|
||||
return this.activeItemPath.some((path) => path.key === processedItem.key);
|
||||
},
|
||||
isItemVisible(processedItem) {
|
||||
return this.getItemProp(processedItem, 'visible') !== false;
|
||||
},
|
||||
isItemDisabled(processedItem) {
|
||||
return this.getItemProp(processedItem, 'disabled');
|
||||
},
|
||||
isItemFocused(processedItem) {
|
||||
return this.focusedItemId === this.getItemId(processedItem);
|
||||
},
|
||||
isItemGroup(processedItem) {
|
||||
return ObjectUtils.isNotEmpty(processedItem.items);
|
||||
},
|
||||
onItemClick(event, processedItem) {
|
||||
const command = this.getItemProp(processedItem, 'command');
|
||||
|
||||
command && command({ originalEvent: event, item: processedItem.item });
|
||||
this.$emit('item-click', { originalEvent: event, processedItem, isFocus: true });
|
||||
},
|
||||
onItemMouseEnter(event, processedItem) {
|
||||
this.$emit('item-mouseenter', { originalEvent: event, processedItem });
|
||||
},
|
||||
onItemActionClick(event, navigate) {
|
||||
navigate && navigate(event);
|
||||
},
|
||||
getAriaSetSize() {
|
||||
return this.items.filter((processedItem) => this.isItemVisible(processedItem) && !this.getItemProp(processedItem, 'separator')).length;
|
||||
},
|
||||
getAriaPosInset(index) {
|
||||
return index - this.items.slice(0, index).filter((processedItem) => this.isItemVisible(processedItem) && this.getItemProp(processedItem, 'separator')).length + 1;
|
||||
},
|
||||
onEnter() {
|
||||
this.position();
|
||||
|
@ -136,38 +164,31 @@ export default {
|
|||
this.$refs.container.style.left = itemOuterWidth + 'px';
|
||||
}
|
||||
},
|
||||
getItemClass(item) {
|
||||
getItemClass(processedItem) {
|
||||
return [
|
||||
'p-menuitem',
|
||||
item.class,
|
||||
this.getItemProp(processedItem, 'class'),
|
||||
{
|
||||
'p-menuitem-active': this.activeItem === item
|
||||
'p-menuitem-active p-highlight': this.isItemActive(processedItem),
|
||||
'p-focus': this.isItemFocused(processedItem),
|
||||
'p-disabled': this.isItemDisabled(processedItem)
|
||||
}
|
||||
];
|
||||
},
|
||||
linkClass(item, routerProps) {
|
||||
getItemActionClass(processedItem, routerProps) {
|
||||
return [
|
||||
'p-menuitem-link',
|
||||
{
|
||||
'p-disabled': this.disabled(item),
|
||||
'router-link-active': routerProps && routerProps.isActive,
|
||||
'router-link-active-exact': this.exact && routerProps && routerProps.isExactActive
|
||||
}
|
||||
];
|
||||
},
|
||||
visible(item) {
|
||||
return typeof item.visible === 'function' ? item.visible() : item.visible !== false;
|
||||
getItemIconClass(processedItem) {
|
||||
return ['p-menuitem-icon', this.getItemProp(processedItem, 'icon')];
|
||||
},
|
||||
disabled(item) {
|
||||
return typeof item.disabled === 'function' ? item.disabled() : item.disabled;
|
||||
},
|
||||
label(item) {
|
||||
return typeof item.label === 'function' ? item.label() : item.label;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerClass() {
|
||||
return { 'p-submenu-list': !this.root };
|
||||
getSeparatorItemClass(processedItem) {
|
||||
return ['p-menuitem-separator', this.getItemProp(processedItem, 'class')];
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
|
|
|
@ -18,25 +18,25 @@
|
|||
/>
|
||||
<component v-else-if="column.children && column.children.body && !column.children.editor && d_editing" :is="column.children.body" :data="editingRowData" :column="column" :field="field" :index="rowIndex" :frozenRow="frozenRow" />
|
||||
<template v-else-if="columnProp('selectionMode')">
|
||||
<DTRadioButton v-if="columnProp('selectionMode') === 'single'" :value="rowData" :checked="selected" @change="toggleRowWithRadio($event, rowIndex)" />
|
||||
<DTCheckbox v-else-if="columnProp('selectionMode') === 'multiple'" :value="rowData" :checked="selected" @change="toggleRowWithCheckbox($event, rowIndex)" />
|
||||
<DTRadioButton v-if="columnProp('selectionMode') === 'single'" :value="rowData" :name="name" :checked="selected" @change="toggleRowWithRadio($event, rowIndex)" />
|
||||
<DTCheckbox v-else-if="columnProp('selectionMode') === 'multiple'" :value="rowData" :checked="selected" :aria-selected="selected ? true : undefined" @change="toggleRowWithCheckbox($event, rowIndex)" />
|
||||
</template>
|
||||
<template v-else-if="columnProp('rowReorder')">
|
||||
<i :class="['p-datatable-reorderablerow-handle', columnProp('rowReorderIcon') || 'pi pi-bars']"></i>
|
||||
</template>
|
||||
<template v-else-if="columnProp('expander')">
|
||||
<button v-ripple class="p-row-toggler p-link" @click="toggleRow" type="button">
|
||||
<button v-ripple class="p-row-toggler p-link" type="button" :aria-expanded="isRowExpanded" :aria-controls="ariaControls" :aria-label="expandButtonAriaLabel" @click="toggleRow">
|
||||
<span :class="rowTogglerIcon"></span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="editMode === 'row' && columnProp('rowEditor')">
|
||||
<button v-if="!d_editing" v-ripple class="p-row-editor-init p-link" @click="onRowEditInit" type="button">
|
||||
<button v-if="!d_editing" v-ripple class="p-row-editor-init p-link" type="button" :aria-label="initButtonAriaLabel" @click="onRowEditInit">
|
||||
<span class="p-row-editor-init-icon pi pi-fw pi-pencil"></span>
|
||||
</button>
|
||||
<button v-if="d_editing" v-ripple class="p-row-editor-save p-link" @click="onRowEditSave" type="button">
|
||||
<button v-if="d_editing" v-ripple class="p-row-editor-save p-link" type="button" :aria-label="saveButtonAriaLabel" @click="onRowEditSave">
|
||||
<span class="p-row-editor-save-icon pi pi-fw pi-check"></span>
|
||||
</button>
|
||||
<button v-if="d_editing" v-ripple class="p-row-editor-cancel p-link" @click="onRowEditCancel" type="button">
|
||||
<button v-if="d_editing" v-ripple class="p-row-editor-cancel p-link" type="button" :aria-label="cancelButtonAriaLabel" @click="onRowEditCancel">
|
||||
<span class="p-row-editor-cancel-icon pi pi-fw pi-times"></span>
|
||||
</button>
|
||||
</template>
|
||||
|
@ -45,11 +45,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler, ObjectUtils } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import RowRadioButton from './RowRadioButton.vue';
|
||||
import RowCheckbox from './RowCheckbox.vue';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler, ObjectUtils } from 'primevue/utils';
|
||||
import RowCheckbox from './RowCheckbox.vue';
|
||||
import RowRadioButton from './RowRadioButton.vue';
|
||||
|
||||
export default {
|
||||
name: 'BodyCell',
|
||||
|
@ -102,6 +102,14 @@ export default {
|
|||
virtualScrollerContentProps: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
ariaControls: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
documentEditListener: null,
|
||||
|
@ -110,7 +118,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
d_editing: this.editing,
|
||||
styleObject: {}
|
||||
styleObject: {},
|
||||
isRowExpanded: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -151,6 +160,7 @@ export default {
|
|||
return ObjectUtils.resolveFieldData(this.rowData, this.field);
|
||||
},
|
||||
toggleRow(event) {
|
||||
this.isRowExpanded = !this.isRowExpanded;
|
||||
this.$emit('row-toggle', {
|
||||
originalEvent: event,
|
||||
data: this.rowData
|
||||
|
@ -234,22 +244,25 @@ export default {
|
|||
},
|
||||
onKeyDown(event) {
|
||||
if (this.editMode === 'cell') {
|
||||
switch (event.which) {
|
||||
case 13:
|
||||
switch (event.code) {
|
||||
case 'Enter':
|
||||
this.completeEdit(event, 'enter');
|
||||
break;
|
||||
|
||||
case 27:
|
||||
case 'Escape':
|
||||
this.switchCellToViewMode();
|
||||
this.$emit('cell-edit-cancel', { originalEvent: event, data: this.rowData, field: this.field, index: this.rowIndex });
|
||||
break;
|
||||
|
||||
case 9:
|
||||
case 'Tab':
|
||||
this.completeEdit(event, 'tab');
|
||||
|
||||
if (event.shiftKey) this.moveToPreviousCell(event);
|
||||
else this.moveToNextCell(event);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -422,6 +435,18 @@ export default {
|
|||
field: this.field
|
||||
})
|
||||
);
|
||||
},
|
||||
expandButtonAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? (this.isRowExpanded ? this.$primevue.config.locale.aria.expandRow : this.$primevue.config.locale.aria.collapseRow) : undefined;
|
||||
},
|
||||
initButtonAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.editRow : undefined;
|
||||
},
|
||||
saveButtonAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.saveEdit : undefined;
|
||||
},
|
||||
cancelButtonAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.cancelEdit : undefined;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div :class="containerClass">
|
||||
<div v-if="display === 'row'" class="p-fluid p-column-filter-element">
|
||||
<div v-if="display === 'row'" class="p-fluid p-column-filter-element" v-bind="filterInputProps">
|
||||
<component :is="filterElement" :field="field" :filterModel="filters[field]" :filterCallback="filterCallback" />
|
||||
</div>
|
||||
<button
|
||||
|
@ -8,8 +8,10 @@
|
|||
ref="icon"
|
||||
type="button"
|
||||
class="p-column-filter-menu-button p-link"
|
||||
:aria-label="filterMenuButtonAriaLabel"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="overlayVisible"
|
||||
:aria-controls="overlayId"
|
||||
:class="{ 'p-column-filter-menu-button-open': overlayVisible, 'p-column-filter-menu-button-active': hasFilter() }"
|
||||
@click="toggleMenu()"
|
||||
@keydown="onToggleButtonKeyDown($event)"
|
||||
|
@ -19,7 +21,18 @@
|
|||
<button v-if="showClearButton && display === 'row'" :class="{ 'p-hidden-space': !hasRowFilter() }" type="button" class="p-column-filter-clear-button p-link" @click="clearFilter()"><span class="pi pi-filter-slash"></span></button>
|
||||
<Portal>
|
||||
<transition name="p-connected-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave">
|
||||
<div v-if="overlayVisible" :ref="overlayRef" :class="overlayClass" @keydown.escape="onEscape" @click="onContentClick" @mousedown="onContentMouseDown">
|
||||
<div
|
||||
v-if="overlayVisible"
|
||||
:ref="overlayRef"
|
||||
:id="overlayId"
|
||||
v-focustrap="{ autoFocus: true }"
|
||||
:aria-modal="overlayVisible"
|
||||
role="dialog"
|
||||
:class="overlayClass"
|
||||
@keydown.escape="hide"
|
||||
@click="onContentClick"
|
||||
@mousedown="onContentMouseDown"
|
||||
>
|
||||
<component :is="filterHeaderTemplate" :field="field" :filterModel="filters[field]" :filterCallback="filterCallback" />
|
||||
<template v-if="display === 'row'">
|
||||
<ul class="p-column-filter-row-items">
|
||||
|
@ -41,7 +54,15 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<div v-if="isShowOperator" class="p-column-filter-operator">
|
||||
<CFDropdown :options="operatorOptions" :modelValue="operator" @update:modelValue="onOperatorChange($event)" class="p-column-filter-operator-dropdown" optionLabel="label" optionValue="value"></CFDropdown>
|
||||
<CFDropdown
|
||||
:options="operatorOptions"
|
||||
:modelValue="operator"
|
||||
:aria-label="filterOperatorAriaLabel"
|
||||
class="p-column-filter-operator-dropdown"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
@update:modelValue="onOperatorChange($event)"
|
||||
></CFDropdown>
|
||||
</div>
|
||||
<div class="p-column-filter-constraints">
|
||||
<div v-for="(fieldConstraint, i) of fieldConstraints" :key="i" class="p-column-filter-constraint">
|
||||
|
@ -49,10 +70,11 @@
|
|||
v-if="isShowMatchModes"
|
||||
:options="matchModes"
|
||||
:modelValue="fieldConstraint.matchMode"
|
||||
class="p-column-filter-matchmode-dropdown"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:aria-label="filterConstraintAriaLabel"
|
||||
@update:modelValue="onMenuMatchModeChange($event, i)"
|
||||
class="p-column-filter-matchmode-dropdown"
|
||||
></CFDropdown>
|
||||
<component v-if="display === 'menu'" :is="filterElement" :field="field" :filterModel="fieldConstraint" :filterCallback="filterCallback" />
|
||||
<div>
|
||||
|
@ -71,10 +93,10 @@
|
|||
<CFButton type="button" :label="addRuleButtonLabel" icon="pi pi-plus" class="p-column-filter-add-button p-button-text p-button-sm" @click="addConstraint()"></CFButton>
|
||||
</div>
|
||||
<div class="p-column-filter-buttonbar">
|
||||
<CFButton v-if="!filterClearTemplate && showClearButton" type="button" class="p-button-outlined p-button-sm" @click="clearFilter()" :label="clearButtonLabel"></CFButton>
|
||||
<CFButton v-if="!filterClearTemplate && showClearButton" type="button" class="p-button-outlined p-button-sm" :label="clearButtonLabel" @click="clearFilter"></CFButton>
|
||||
<component v-else :is="filterClearTemplate" :field="field" :filterModel="filters[field]" :filterCallback="clearFilter" />
|
||||
<template v-if="showApplyButton">
|
||||
<CFButton v-if="!filterApplyTemplate" type="button" class="p-button-sm" @click="applyFilter()" :label="applyButtonLabel"></CFButton>
|
||||
<CFButton v-if="!filterApplyTemplate" type="button" class="p-button-sm" :label="applyButtonLabel" @click="applyFilter()"></CFButton>
|
||||
<component v-else :is="filterApplyTemplate" :field="field" :filterModel="filters[field]" :filterCallback="applyFilter" />
|
||||
</template>
|
||||
</div>
|
||||
|
@ -87,12 +109,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler, ConnectedOverlayScrollHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import { FilterOperator } from 'primevue/api';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import Button from 'primevue/button';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import FocusTrap from 'primevue/focustrap';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Portal from 'primevue/portal';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'ColumnFilter',
|
||||
|
@ -166,6 +189,10 @@ export default {
|
|||
filterMenuStyle: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
filterInputProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -251,34 +278,16 @@ export default {
|
|||
this.overlayVisible = !this.overlayVisible;
|
||||
},
|
||||
onToggleButtonKeyDown(event) {
|
||||
switch (event.key) {
|
||||
switch (event.code) {
|
||||
case 'Enter':
|
||||
case 'Space':
|
||||
this.toggleMenu();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
case 'Tab':
|
||||
this.overlayVisible = false;
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
if (this.overlayVisible) {
|
||||
let focusable = DomHandler.getFocusableElements(this.overlay);
|
||||
|
||||
if (focusable) {
|
||||
focusable[0].focus();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.altKey) {
|
||||
this.overlayVisible = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
onEscape() {
|
||||
this.overlayVisible = false;
|
||||
|
||||
if (this.$refs.icon) {
|
||||
this.$refs.icon.focus();
|
||||
}
|
||||
},
|
||||
onRowMatchModeChange(matchMode) {
|
||||
|
@ -293,7 +302,7 @@ export default {
|
|||
onRowMatchModeKeyDown(event) {
|
||||
let item = event.target;
|
||||
|
||||
switch (event.key) {
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
var nextItem = this.findNextItem(item);
|
||||
|
||||
|
@ -379,11 +388,13 @@ export default {
|
|||
findPrevItem(item) {
|
||||
let prevItem = item.previousElementSibling;
|
||||
|
||||
if (prevItem) DomHandler.hasClass(prevItem, 'p-column-filter-separator') ? this.findPrevItem(prevItem) : prevItem;
|
||||
if (prevItem) return DomHandler.hasClass(prevItem, 'p-column-filter-separator') ? this.findPrevItem(prevItem) : prevItem;
|
||||
else return item.parentElement.lastElementChild;
|
||||
},
|
||||
hide() {
|
||||
this.overlayVisible = false;
|
||||
|
||||
DomHandler.focus(this.$refs.icon);
|
||||
},
|
||||
onContentClick(event) {
|
||||
this.selfClick = true;
|
||||
|
@ -516,6 +527,9 @@ export default {
|
|||
showMenuButton() {
|
||||
return this.showMenu && (this.display === 'row' ? this.type !== 'boolean' : true);
|
||||
},
|
||||
overlayId() {
|
||||
return UniqueComponentId();
|
||||
},
|
||||
matchModes() {
|
||||
return (
|
||||
this.matchModeOptions ||
|
||||
|
@ -534,7 +548,7 @@ export default {
|
|||
];
|
||||
},
|
||||
noFilterLabel() {
|
||||
return this.$primevue.config.locale.noFilter;
|
||||
return this.$primevue.config.locale ? this.$primevue.config.locale.noFilter : undefined;
|
||||
},
|
||||
isShowOperator() {
|
||||
return this.showOperator && this.filters[this.field].operator;
|
||||
|
@ -549,25 +563,37 @@ export default {
|
|||
return this.fieldConstraints.length > 1;
|
||||
},
|
||||
removeRuleButtonLabel() {
|
||||
return this.$primevue.config.locale.removeRule;
|
||||
return this.$primevue.config.locale ? this.$primevue.config.locale.removeRule : undefined;
|
||||
},
|
||||
addRuleButtonLabel() {
|
||||
return this.$primevue.config.locale.addRule;
|
||||
return this.$primevue.config.locale ? this.$primevue.config.locale.addRule : undefined;
|
||||
},
|
||||
isShowAddConstraint() {
|
||||
return this.showAddButton && this.filters[this.field].operator && this.fieldConstraints && this.fieldConstraints.length < this.maxConstraints;
|
||||
},
|
||||
clearButtonLabel() {
|
||||
return this.$primevue.config.locale.clear;
|
||||
return this.$primevue.config.locale ? this.$primevue.config.locale.clear : undefined;
|
||||
},
|
||||
applyButtonLabel() {
|
||||
return this.$primevue.config.locale.apply;
|
||||
return this.$primevue.config.locale ? this.$primevue.config.locale.apply : undefined;
|
||||
},
|
||||
filterMenuButtonAriaLabel() {
|
||||
return this.$primevue.config.locale ? (this.overlayVisible ? this.$primevue.config.locale.showFilterMenu : this.$primevue.config.locale.hideFilterMenu) : undefined;
|
||||
},
|
||||
filterOperatorAriaLabel() {
|
||||
return this.$primevue.config.locale ? this.$primevue.config.locale.filterOperator : undefined;
|
||||
},
|
||||
filterConstraintAriaLabel() {
|
||||
return this.$primevue.config.locale ? this.$primevue.config.locale.filterConstraint : undefined;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CFDropdown: Dropdown,
|
||||
CFButton: Button,
|
||||
Portal: Portal
|
||||
},
|
||||
directives: {
|
||||
focustrap: FocusTrap
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { VNode } from 'vue';
|
||||
import { InputHTMLAttributes, TableHTMLAttributes, VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor, Nullable } from '../ts-helpers';
|
||||
import Column from '../column';
|
||||
import { VirtualScrollerProps } from '../virtualscroller';
|
||||
|
||||
type DataTablePaginatorPositionType = 'top' | 'bottom' | 'both' | undefined;
|
||||
|
@ -511,7 +510,7 @@ export interface DataTableProps {
|
|||
* - JumpToPageInput
|
||||
* - CurrentPageReport
|
||||
*/
|
||||
paginatorTemplate?: string | undefined;
|
||||
paginatorTemplate?: any | string;
|
||||
/**
|
||||
* Number of page links to display.
|
||||
* Default value is 5.
|
||||
|
@ -685,7 +684,7 @@ export interface DataTableProps {
|
|||
/**
|
||||
* One or more field names to use in row grouping.
|
||||
*/
|
||||
groupRowsBy?: string[] | string | undefined;
|
||||
groupRowsBy?: (field: string) => object | string[] | string | undefined;
|
||||
/**
|
||||
* Whether the row groups can be expandable.
|
||||
*/
|
||||
|
@ -776,6 +775,14 @@ export interface DataTableProps {
|
|||
* Style class of the table element.
|
||||
*/
|
||||
tableClass?: any;
|
||||
/**
|
||||
* Uses to pass all properties of the TableHTMLAttributes to table element inside the component.
|
||||
*/
|
||||
tableProps?: TableHTMLAttributes | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLInputElement to the focusable filter input element inside the component.
|
||||
*/
|
||||
filterInputProps?: InputHTMLAttributes | undefined;
|
||||
}
|
||||
|
||||
export interface DataTableSlots {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import Button from '@/components/button/Button.vue';
|
||||
import Column from '@/components/column/Column.vue';
|
||||
import ColumnGroup from '@/components/columngroup/ColumnGroup.vue';
|
||||
import InputText from '@/components/inputtext/InputText.vue';
|
||||
import Row from '@/components/row/Row.vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import DataTable from './DataTable.vue';
|
||||
import ColumnGroup from '../columngroup/ColumnGroup.vue';
|
||||
import Row from '../row/Row.vue';
|
||||
import Column from '../column/Column.vue';
|
||||
import Button from '../button/Button.vue';
|
||||
import InputText from '../inputtext/InputText.vue';
|
||||
import { FilterMatchMode } from 'primevue/api';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import DataTable from './DataTable.vue';
|
||||
|
||||
window.URL.createObjectURL = function () {};
|
||||
|
||||
|
@ -86,6 +87,7 @@ describe('DataTable.vue', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column,
|
||||
Button
|
||||
|
@ -284,6 +286,7 @@ describe('DataTable.vue', () => {
|
|||
it('should single sort', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -301,7 +304,7 @@ describe('DataTable.vue', () => {
|
|||
|
||||
const sortableTH = wrapper.findAll('.p-sortable-column')[0];
|
||||
const firstCellText = wrapper.findAll('.p-datatable-tbody > tr')[0].findAll('td')[1].text();
|
||||
const headerClick = vi.spyOn(wrapper.vm, 'onColumnHeaderClick');
|
||||
const headerClick = jest.spyOn(wrapper.vm, 'onColumnHeaderClick');
|
||||
|
||||
await sortableTH.trigger('click');
|
||||
|
||||
|
@ -315,6 +318,7 @@ describe('DataTable.vue', () => {
|
|||
it('should multiple sort', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -333,7 +337,7 @@ describe('DataTable.vue', () => {
|
|||
|
||||
const sortableTHs = wrapper.findAll('.p-sortable-column');
|
||||
const firstCellText = wrapper.findAll('.p-datatable-tbody > tr')[0].findAll('td')[1].text();
|
||||
const headerClick = vi.spyOn(wrapper.vm, 'onColumnHeaderClick');
|
||||
const headerClick = jest.spyOn(wrapper.vm, 'onColumnHeaderClick');
|
||||
|
||||
await sortableTHs[0].trigger('click');
|
||||
|
||||
|
@ -355,6 +359,7 @@ describe('DataTable.vue', () => {
|
|||
it('should have presort', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -381,6 +386,7 @@ describe('DataTable.vue', () => {
|
|||
it('should remove sort', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -509,6 +515,7 @@ describe('DataTable.vue', () => {
|
|||
it('should select when radiobutton selection is enabled', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -544,6 +551,7 @@ describe('DataTable.vue', () => {
|
|||
it('should select when checkbox selection is enabled', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -577,6 +585,7 @@ describe('DataTable.vue', () => {
|
|||
it('should select all rows', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -603,6 +612,7 @@ describe('DataTable.vue', () => {
|
|||
it('should unselect all rows', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -663,6 +673,7 @@ describe('DataTable.vue', () => {
|
|||
wrapper = null;
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -719,6 +730,7 @@ describe('DataTable.vue', () => {
|
|||
it('should init row editing', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column,
|
||||
InputText
|
||||
|
@ -759,6 +771,7 @@ describe('DataTable.vue', () => {
|
|||
it('should save row editing', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column,
|
||||
InputText
|
||||
|
@ -795,6 +808,7 @@ describe('DataTable.vue', () => {
|
|||
it('should cancel row editing', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column,
|
||||
InputText
|
||||
|
@ -832,6 +846,7 @@ describe('DataTable.vue', () => {
|
|||
it('should fit mode expanding exists', () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -857,6 +872,7 @@ describe('DataTable.vue', () => {
|
|||
it('should fit mode resize start', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -885,6 +901,7 @@ describe('DataTable.vue', () => {
|
|||
it('should fit mode resize', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -910,6 +927,7 @@ describe('DataTable.vue', () => {
|
|||
it('should fit mode column resize end', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -939,6 +957,7 @@ describe('DataTable.vue', () => {
|
|||
it('should expand mode resize start', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -967,6 +986,7 @@ describe('DataTable.vue', () => {
|
|||
it('should fit mode resize', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -992,6 +1012,7 @@ describe('DataTable.vue', () => {
|
|||
it('should fit mode column resize end', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -1030,6 +1051,7 @@ describe('DataTable.vue', () => {
|
|||
it('should exist', () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -1054,6 +1076,7 @@ describe('DataTable.vue', () => {
|
|||
it('should exist', () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -1085,6 +1108,7 @@ describe('DataTable.vue', () => {
|
|||
it('should have groupheader templating', () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -1115,6 +1139,7 @@ describe('DataTable.vue', () => {
|
|||
it('should have groupfooter templating', () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -1145,6 +1170,7 @@ describe('DataTable.vue', () => {
|
|||
it('should have expandable row groups and expand rows', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -1270,6 +1296,7 @@ describe('DataTable.vue', () => {
|
|||
it('should have rowspan grouping', async () => {
|
||||
wrapper = mount(DataTable, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: {
|
||||
Column
|
||||
}
|
||||
|
@ -1338,7 +1365,7 @@ describe('DataTable.vue', () => {
|
|||
|
||||
// export
|
||||
it('should export table', async () => {
|
||||
const exportCSV = vi.spyOn(wrapper.vm, 'exportCSV');
|
||||
const exportCSV = jest.spyOn(wrapper.vm, 'exportCSV');
|
||||
|
||||
await wrapper.vm.exportCSV();
|
||||
|
||||
|
@ -1355,8 +1382,8 @@ describe('DataTable.vue', () => {
|
|||
});
|
||||
|
||||
it('should save session storage', async () => {
|
||||
vi.spyOn(window.sessionStorage.__proto__, 'setItem');
|
||||
window.sessionStorage.__proto__.setItem = vi.fn();
|
||||
jest.spyOn(window.sessionStorage.__proto__, 'setItem');
|
||||
window.sessionStorage.__proto__.setItem = jest.fn();
|
||||
|
||||
await wrapper.vm.saveState();
|
||||
|
||||
|
@ -1365,8 +1392,8 @@ describe('DataTable.vue', () => {
|
|||
});
|
||||
|
||||
it('should save local storage', async () => {
|
||||
vi.spyOn(window.localStorage.__proto__, 'setItem');
|
||||
window.localStorage.__proto__.setItem = vi.fn();
|
||||
jest.spyOn(window.localStorage.__proto__, 'setItem');
|
||||
window.localStorage.__proto__.setItem = jest.fn();
|
||||
|
||||
await wrapper.vm.saveState();
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<div class="p-datatable-wrapper" :style="{ maxHeight: virtualScrollerDisabled ? scrollHeight : '' }">
|
||||
<DTVirtualScroller ref="virtualScroller" v-bind="virtualScrollerOptions" :items="processedData" :columns="columns" :style="{ height: scrollHeight }" :disabled="virtualScrollerDisabled" loaderDisabled :showSpacer="false">
|
||||
<template #content="slotProps">
|
||||
<table ref="table" role="table" :class="[tableClass, 'p-datatable-table']" :style="[tableStyle, slotProps.spacerStyle]">
|
||||
<table ref="table" role="table" :class="[tableClass, 'p-datatable-table']" :style="[tableStyle, slotProps.spacerStyle]" v-bind="tableProps">
|
||||
<DTTableHeader
|
||||
:columnGroup="headerColumnGroup"
|
||||
:columns="slotProps.columns"
|
||||
|
@ -49,6 +49,7 @@
|
|||
:filters="d_filters"
|
||||
:filtersStore="filters"
|
||||
:filterDisplay="filterDisplay"
|
||||
:filterInputProps="filterInputProps"
|
||||
@column-click="onColumnHeaderClick($event)"
|
||||
@column-mousedown="onColumnHeaderMouseDown($event)"
|
||||
@filter-change="onFilterChange"
|
||||
|
@ -90,6 +91,7 @@
|
|||
:editingRowKeys="d_editingRowKeys"
|
||||
:templates="$slots"
|
||||
:responsiveLayout="responsiveLayout"
|
||||
:isVirtualScrollerDisabled="true"
|
||||
@rowgroup-toggle="toggleRowGroup"
|
||||
@row-click="onRowClick($event)"
|
||||
@row-dblclick="onRowDblClick($event)"
|
||||
|
@ -113,7 +115,6 @@
|
|||
@row-edit-cancel="onRowEditCancel($event)"
|
||||
:editingMeta="d_editingMeta"
|
||||
@editing-meta-change="onEditingMetaChange"
|
||||
:isVirtualScrollerDisabled="true"
|
||||
/>
|
||||
<DTTableBody
|
||||
ref="bodyRef"
|
||||
|
@ -144,12 +145,14 @@
|
|||
:editingRowKeys="d_editingRowKeys"
|
||||
:templates="$slots"
|
||||
:responsiveLayout="responsiveLayout"
|
||||
:virtualScrollerContentProps="slotProps"
|
||||
:isVirtualScrollerDisabled="virtualScrollerDisabled"
|
||||
@rowgroup-toggle="toggleRowGroup"
|
||||
@row-click="onRowClick($event)"
|
||||
@row-dblclick="onRowDblClick($event)"
|
||||
@row-rightclick="onRowRightClick($event)"
|
||||
@row-touchend="onRowTouchEnd"
|
||||
@row-keydown="onRowKeyDown"
|
||||
@row-keydown="onRowKeyDown($event, slotProps)"
|
||||
@row-mousedown="onRowMouseDown"
|
||||
@row-dragstart="onRowDragStart($event)"
|
||||
@row-dragover="onRowDragOver($event)"
|
||||
|
@ -167,8 +170,6 @@
|
|||
@row-edit-cancel="onRowEditCancel($event)"
|
||||
:editingMeta="d_editingMeta"
|
||||
@editing-meta-change="onEditingMetaChange"
|
||||
:virtualScrollerContentProps="slotProps"
|
||||
:isVirtualScrollerDisabled="virtualScrollerDisabled"
|
||||
/>
|
||||
<DTTableFooter :columnGroup="footerColumnGroup" :columns="slotProps.columns" />
|
||||
</table>
|
||||
|
@ -205,13 +206,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ObjectUtils, DomHandler, UniqueComponentId } from 'primevue/utils';
|
||||
import { FilterMatchMode, FilterOperator, FilterService } from 'primevue/api';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import VirtualScroller from 'primevue/virtualscroller';
|
||||
import TableHeader from './TableHeader.vue';
|
||||
import TableBody from './TableBody.vue';
|
||||
import TableFooter from './TableFooter.vue';
|
||||
import TableHeader from './TableHeader.vue';
|
||||
|
||||
export default {
|
||||
name: 'DataTable',
|
||||
|
@ -289,7 +290,7 @@ export default {
|
|||
default: true
|
||||
},
|
||||
paginatorTemplate: {
|
||||
type: String,
|
||||
type: [Object, String],
|
||||
default: 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown'
|
||||
},
|
||||
pageLinkSize: {
|
||||
|
@ -433,7 +434,7 @@ export default {
|
|||
default: null
|
||||
},
|
||||
groupRowsBy: {
|
||||
type: [Array, String],
|
||||
type: [Array, String, Function],
|
||||
default: null
|
||||
},
|
||||
expandableRowGroups: {
|
||||
|
@ -511,6 +512,14 @@ export default {
|
|||
tableClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
tableProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
filterInputProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -864,6 +873,9 @@ export default {
|
|||
},
|
||||
onRowClick(e) {
|
||||
const event = e.originalEvent;
|
||||
const index = e.index;
|
||||
const body = this.$refs.bodyRef && this.$refs.bodyRef.$el;
|
||||
const focusedItem = DomHandler.findSingle(body, 'tr.p-selectable-row[tabindex="0"]');
|
||||
|
||||
if (DomHandler.isClickable(event.target)) {
|
||||
return;
|
||||
|
@ -940,6 +952,11 @@ export default {
|
|||
}
|
||||
|
||||
this.rowTouched = false;
|
||||
|
||||
if (focusedItem) {
|
||||
focusedItem.tabIndex = '-1';
|
||||
DomHandler.find(body, 'tr.p-selectable-row')[index].tabIndex = '0';
|
||||
}
|
||||
},
|
||||
onRowDblClick(e) {
|
||||
const event = e.originalEvent;
|
||||
|
@ -960,48 +977,153 @@ export default {
|
|||
onRowTouchEnd() {
|
||||
this.rowTouched = true;
|
||||
},
|
||||
onRowKeyDown(e) {
|
||||
onRowKeyDown(e, slotProps) {
|
||||
const event = e.originalEvent;
|
||||
const rowData = e.data;
|
||||
const rowIndex = e.index;
|
||||
const metaKey = event.metaKey || event.ctrlKey;
|
||||
|
||||
if (this.selectionMode) {
|
||||
const row = event.target;
|
||||
|
||||
switch (event.which) {
|
||||
//down arrow
|
||||
case 40:
|
||||
var nextRow = this.findNextSelectableRow(row);
|
||||
|
||||
if (nextRow) {
|
||||
nextRow.focus();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
this.onArrowDownKey(event, row, rowIndex, slotProps);
|
||||
break;
|
||||
|
||||
//up arrow
|
||||
case 38:
|
||||
var prevRow = this.findPrevSelectableRow(row);
|
||||
|
||||
if (prevRow) {
|
||||
prevRow.focus();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
case 'ArrowUp':
|
||||
this.onArrowUpKey(event, row, rowIndex, slotProps);
|
||||
break;
|
||||
|
||||
//enter
|
||||
case 13:
|
||||
this.onRowClick({ originalEvent: event, data: rowData, index: rowIndex });
|
||||
case 'Home':
|
||||
this.onHomeKey(event, row, rowIndex, slotProps);
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
this.onEndKey(event, row, rowIndex, slotProps);
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
this.onEnterKey(event, rowData, rowIndex);
|
||||
break;
|
||||
|
||||
case 'Space':
|
||||
this.onSpaceKey(event, rowData, rowIndex, slotProps);
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
this.onTabKey(event, rowIndex);
|
||||
break;
|
||||
|
||||
default:
|
||||
//no op
|
||||
if (event.code === 'KeyA' && metaKey) {
|
||||
const data = this.dataToRender(slotProps.rows);
|
||||
|
||||
this.$emit('update:selection', data);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
onArrowDownKey(event, row, rowIndex, slotProps) {
|
||||
const nextRow = this.findNextSelectableRow(row);
|
||||
|
||||
nextRow && this.focusRowChange(row, nextRow);
|
||||
|
||||
if (event.shiftKey) {
|
||||
const data = this.dataToRender(slotProps.rows);
|
||||
const nextRowIndex = rowIndex + 1 >= data.length ? data.length - 1 : rowIndex + 1;
|
||||
|
||||
this.onRowClick({ originalEvent: event, data: data[nextRowIndex], index: nextRowIndex });
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onArrowUpKey(event, row, rowIndex, slotProps) {
|
||||
const prevRow = this.findPrevSelectableRow(row);
|
||||
|
||||
prevRow && this.focusRowChange(row, prevRow);
|
||||
|
||||
if (event.shiftKey) {
|
||||
const data = this.dataToRender(slotProps.rows);
|
||||
const prevRowIndex = rowIndex - 1 <= 0 ? 0 : rowIndex - 1;
|
||||
|
||||
this.onRowClick({ originalEvent: event, data: data[prevRowIndex], index: prevRowIndex });
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onHomeKey(event, row, rowIndex, slotProps) {
|
||||
const firstRow = this.findFirstSelectableRow();
|
||||
|
||||
firstRow && this.focusRowChange(row, firstRow);
|
||||
|
||||
if (event.ctrlKey && event.shiftKey) {
|
||||
const data = this.dataToRender(slotProps.rows);
|
||||
|
||||
this.$emit('update:selection', data.slice(0, rowIndex + 1));
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onEndKey(event, row, rowIndex, slotProps) {
|
||||
const lastRow = this.findLastSelectableRow();
|
||||
|
||||
lastRow && this.focusRowChange(row, lastRow);
|
||||
|
||||
if (event.ctrlKey && event.shiftKey) {
|
||||
const data = this.dataToRender(slotProps.rows);
|
||||
|
||||
this.$emit('update:selection', data.slice(rowIndex, data.length));
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onEnterKey(event, rowData, rowIndex) {
|
||||
this.onRowClick({ originalEvent: event, data: rowData, index: rowIndex });
|
||||
event.preventDefault();
|
||||
},
|
||||
onSpaceKey(event, rowData, rowIndex, slotProps) {
|
||||
this.onEnterKey(event, rowData, rowIndex);
|
||||
|
||||
if (event.shiftKey && this.selection !== null) {
|
||||
const data = this.dataToRender(slotProps.rows);
|
||||
let index;
|
||||
|
||||
if (this.selection.length > 0) {
|
||||
let firstSelectedRowIndex, lastSelectedRowIndex;
|
||||
|
||||
firstSelectedRowIndex = ObjectUtils.findIndexInList(this.selection[0], data);
|
||||
lastSelectedRowIndex = ObjectUtils.findIndexInList(this.selection[this.selection.length - 1], data);
|
||||
|
||||
index = rowIndex <= firstSelectedRowIndex ? lastSelectedRowIndex : firstSelectedRowIndex;
|
||||
} else {
|
||||
index = ObjectUtils.findIndexInList(this.selection, data);
|
||||
}
|
||||
|
||||
const _selection = index !== rowIndex ? data.slice(Math.min(index, rowIndex), Math.max(index, rowIndex) + 1) : rowData;
|
||||
|
||||
this.$emit('update:selection', _selection);
|
||||
}
|
||||
},
|
||||
onTabKey(event, rowIndex) {
|
||||
const body = this.$refs.bodyRef && this.$refs.bodyRef.$el;
|
||||
const rows = DomHandler.find(body, 'tr.p-selectable-row');
|
||||
|
||||
if (event.code === 'Tab' && rows && rows.length > 0) {
|
||||
const firstSelectedRow = DomHandler.findSingle(body, 'tr.p-highlight');
|
||||
const focusedItem = DomHandler.findSingle(body, 'tr.p-selectable-row[tabindex="0"]');
|
||||
|
||||
if (firstSelectedRow) {
|
||||
firstSelectedRow.tabIndex = '0';
|
||||
focusedItem !== firstSelectedRow && (focusedItem.tabIndex = '-1');
|
||||
} else {
|
||||
rows[0].tabIndex = '0';
|
||||
focusedItem !== rows[0] && (rows[rowIndex].tabIndex = '-1');
|
||||
}
|
||||
}
|
||||
},
|
||||
findNextSelectableRow(row) {
|
||||
let nextRow = row.nextElementSibling;
|
||||
|
||||
|
@ -1022,6 +1144,21 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
findFirstSelectableRow() {
|
||||
const firstRow = DomHandler.findSingle(this.$refs.table, '.p-selectable-row');
|
||||
|
||||
return firstRow;
|
||||
},
|
||||
findLastSelectableRow() {
|
||||
const rows = DomHandler.find(this.$refs.table, '.p-selectable-row');
|
||||
|
||||
return rows ? rows[rows.length - 1] : null;
|
||||
},
|
||||
focusRowChange(firstFocusableRow, currentFocusedRow) {
|
||||
firstFocusableRow.tabIndex = '-1';
|
||||
currentFocusedRow.tabIndex = '0';
|
||||
DomHandler.focus(currentFocusedRow);
|
||||
},
|
||||
toggleRowWithRadio(event) {
|
||||
const rowData = event.data;
|
||||
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
:style="containerStyle"
|
||||
:class="containerClass"
|
||||
:tabindex="columnProp('sortable') ? '0' : null"
|
||||
role="cell"
|
||||
role="columnheader"
|
||||
:colspan="columnProp('colspan')"
|
||||
:rowspan="columnProp('rowspan')"
|
||||
:aria-sort="ariaSort"
|
||||
@click="onClick"
|
||||
@keydown="onKeyDown"
|
||||
@mousedown="onMouseDown"
|
||||
|
@ -11,9 +14,6 @@
|
|||
@dragover="onDragOver"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop"
|
||||
:colspan="columnProp('colspan')"
|
||||
:rowspan="columnProp('rowspan')"
|
||||
:aria-sort="ariaSort"
|
||||
>
|
||||
<span v-if="resizableColumns && !columnProp('frozen')" class="p-column-resizer" @mousedown="onResizeStart"></span>
|
||||
<div class="p-column-header-content">
|
||||
|
@ -35,6 +35,7 @@
|
|||
:filterApplyTemplate="column.children && column.children.filterapply"
|
||||
:filters="filters"
|
||||
:filtersStore="filtersStore"
|
||||
:filterInputProps="filterInputProps"
|
||||
@filter-change="$emit('filter-change', $event)"
|
||||
@filter-apply="$emit('filter-apply')"
|
||||
:filterMenuStyle="columnProp('filterMenuStyle')"
|
||||
|
@ -58,8 +59,8 @@
|
|||
|
||||
<script>
|
||||
import { DomHandler, ObjectUtils } from 'primevue/utils';
|
||||
import HeaderCheckbox from './HeaderCheckbox.vue';
|
||||
import ColumnFilter from './ColumnFilter.vue';
|
||||
import HeaderCheckbox from './HeaderCheckbox.vue';
|
||||
|
||||
export default {
|
||||
name: 'HeaderCell',
|
||||
|
@ -91,7 +92,7 @@ export default {
|
|||
default: false
|
||||
},
|
||||
groupRowsBy: {
|
||||
type: [Array, String],
|
||||
type: [Array, String, Function],
|
||||
default: null
|
||||
},
|
||||
sortMode: {
|
||||
|
@ -141,6 +142,10 @@ export default {
|
|||
reorderableColumns: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filterInputProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -166,8 +171,9 @@ export default {
|
|||
this.$emit('column-click', { originalEvent: event, column: this.column });
|
||||
},
|
||||
onKeyDown(event) {
|
||||
if (event.which === 13 && event.currentTarget.nodeName === 'TH' && DomHandler.hasClass(event.currentTarget, 'p-sortable-column')) {
|
||||
if ((event.code === 'Enter' || event.code === 'Space') && event.currentTarget.nodeName === 'TH' && DomHandler.hasClass(event.currentTarget, 'p-sortable-column')) {
|
||||
this.$emit('column-click', { originalEvent: event, column: this.column });
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onMouseDown(event) {
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
<template>
|
||||
<div :class="['p-checkbox p-component', { 'p-checkbox-focused': focused, 'p-disabled': $attrs.disabled }]" @click="onClick" @keydown.space.prevent="onClick">
|
||||
<div
|
||||
ref="box"
|
||||
:class="['p-checkbox-box p-component', { 'p-highlight': checked, 'p-disabled': $attrs.disabled, 'p-focus': focused }]"
|
||||
role="checkbox"
|
||||
:aria-checked="checked"
|
||||
:tabindex="$attrs.disabled ? null : '0'"
|
||||
@focus="onFocus($event)"
|
||||
@blur="onBlur($event)"
|
||||
>
|
||||
<div :class="['p-checkbox p-component', { 'p-checkbox-focused': focused, 'p-disabled': disabled }]" @click="onClick" @keydown.space.prevent="onClick">
|
||||
<div class="p-hidden-accessible">
|
||||
<input ref="input" type="checkbox" :checked="checked" :disabled="disabled" :tabindex="disabled ? null : '0'" :aria-label="headerCheckboxAriaLabel" @focus="onFocus($event)" @blur="onBlur($event)" />
|
||||
</div>
|
||||
<div ref="box" :class="['p-checkbox-box p-component', { 'p-highlight': checked, 'p-disabled': disabled, 'p-focus': focused }]">
|
||||
<span :class="['p-checkbox-icon', { 'pi pi-check': checked }]"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'HeaderCheckbox',
|
||||
inheritAttrs: false,
|
||||
emits: ['change'],
|
||||
props: {
|
||||
checked: null
|
||||
checked: null,
|
||||
disabled: null
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -29,12 +26,13 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onClick(event) {
|
||||
if (!this.$attrs.disabled) {
|
||||
this.focused = true;
|
||||
if (!this.disabled) {
|
||||
this.$emit('change', {
|
||||
originalEvent: event,
|
||||
checked: !this.checked
|
||||
});
|
||||
|
||||
DomHandler.focus(this.$refs.input);
|
||||
}
|
||||
},
|
||||
onFocus() {
|
||||
|
@ -43,6 +41,11 @@ export default {
|
|||
onBlur() {
|
||||
this.focused = false;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerCheckboxAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? (this.checked ? this.$primevue.config.locale.aria.selectAll : this.$primevue.config.locale.aria.unselectAll) : undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
<template>
|
||||
<div :class="['p-checkbox p-component', { 'p-checkbox-focused': focused }]" @click.stop.prevent="onClick">
|
||||
<div
|
||||
ref="box"
|
||||
:class="['p-checkbox-box p-component', { 'p-highlight': checked, 'p-disabled': $attrs.disabled, 'p-focus': focused }]"
|
||||
role="checkbox"
|
||||
:aria-checked="checked"
|
||||
:tabindex="$attrs.disabled ? null : '0'"
|
||||
@keydown.space.prevent="onClick"
|
||||
@focus="onFocus($event)"
|
||||
@blur="onBlur($event)"
|
||||
>
|
||||
<div :class="['p-checkbox p-component', { 'p-checkbox-focused': focused }]" @click="onClick">
|
||||
<div class="p-hidden-accessible">
|
||||
<input ref="input" type="checkbox" :checked="checked" :disabled="$attrs.disabled" :tabindex="$attrs.disabled ? null : '0'" :aria-label="checkboxAriaLabel" @focus="onFocus($event)" @blur="onBlur($event)" @keydown="onKeydown" />
|
||||
</div>
|
||||
<div ref="box" :class="['p-checkbox-box p-component', { 'p-highlight': checked, 'p-disabled': $attrs.disabled, 'p-focus': focused }]">
|
||||
<span :class="['p-checkbox-icon', { 'pi pi-check': checked }]"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'RowCheckbox',
|
||||
inheritAttrs: false,
|
||||
emits: ['change'],
|
||||
props: {
|
||||
value: null,
|
||||
|
@ -32,18 +27,38 @@ export default {
|
|||
methods: {
|
||||
onClick(event) {
|
||||
if (!this.$attrs.disabled) {
|
||||
this.focused = true;
|
||||
this.$emit('change', {
|
||||
originalEvent: event,
|
||||
data: this.value
|
||||
});
|
||||
|
||||
DomHandler.focus(this.$refs.input);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
onFocus() {
|
||||
this.focused = true;
|
||||
},
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
},
|
||||
onKeydown(event) {
|
||||
switch (event.code) {
|
||||
case 'Space': {
|
||||
this.onClick(event);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkboxAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? (this.checked ? this.$primevue.config.locale.aria.selectRow : this.$primevue.config.locale.aria.unselectRow) : undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
<template>
|
||||
<div :class="['p-radiobutton p-component', { 'p-radiobutton-focused': focused }]" @click="onClick" tabindex="0" @focus="onFocus($event)" @blur="onBlur($event)" @keydown.space.prevent="onClick">
|
||||
<div ref="box" :class="['p-radiobutton-box p-component', { 'p-highlight': checked, 'p-disabled': $attrs.disabled, 'p-focus': focused }]" role="radio" :aria-checked="checked">
|
||||
<div :class="['p-radiobutton p-component', { 'p-radiobutton-focused': focused }]" @click="onClick">
|
||||
<div class="p-hidden-accessible">
|
||||
<input ref="input" type="radio" :checked="checked" :disabled="$attrs.disabled" :name="name" tabindex="0" @focus="onFocus($event)" @blur="onBlur($event)" @keydown.space.prevent="onClick" />
|
||||
</div>
|
||||
<div ref="box" :class="['p-radiobutton-box p-component', { 'p-highlight': checked, 'p-disabled': $attrs.disabled, 'p-focus': focused }]">
|
||||
<div class="p-radiobutton-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'RowRadioButton',
|
||||
inheritAttrs: false,
|
||||
emits: ['change'],
|
||||
props: {
|
||||
value: null,
|
||||
checked: null
|
||||
checked: null,
|
||||
name: null
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -28,6 +34,8 @@ export default {
|
|||
originalEvent: event,
|
||||
data: this.value
|
||||
});
|
||||
|
||||
DomHandler.focus(this.$refs.input);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -15,19 +15,20 @@
|
|||
:key="getRowKey(rowData, getRowIndex(index))"
|
||||
:class="getRowClass(rowData)"
|
||||
:style="rowStyle"
|
||||
:tabindex="setRowTabindex(index)"
|
||||
role="row"
|
||||
:aria-selected="selectionMode ? isSelected(rowData) : null"
|
||||
@click="onRowClick($event, rowData, getRowIndex(index))"
|
||||
@dblclick="onRowDblClick($event, rowData, getRowIndex(index))"
|
||||
@contextmenu="onRowRightClick($event, rowData, getRowIndex(index))"
|
||||
@touchend="onRowTouchEnd($event)"
|
||||
@keydown="onRowKeyDown($event, rowData, getRowIndex(index))"
|
||||
:tabindex="selectionMode || contextMenu ? '0' : null"
|
||||
@mousedown="onRowMouseDown($event)"
|
||||
@dragstart="onRowDragStart($event, getRowIndex(index))"
|
||||
@dragover="onRowDragOver($event, getRowIndex(index))"
|
||||
@dragleave="onRowDragLeave($event)"
|
||||
@dragend="onRowDragEnd($event)"
|
||||
@drop="onRowDrop($event)"
|
||||
role="row"
|
||||
>
|
||||
<template v-for="(col, i) of columns" :key="columnProp(col, 'columnKey') || columnProp(col, 'field') || i">
|
||||
<DTBodyCell
|
||||
|
@ -42,7 +43,11 @@
|
|||
:rowspan="rowGroupMode === 'rowspan' ? calculateRowGroupSize(value, col, getRowIndex(index)) : null"
|
||||
:editMode="editMode"
|
||||
:editing="editMode === 'row' && isRowEditing(rowData)"
|
||||
:editingMeta="editingMeta"
|
||||
:responsiveLayout="responsiveLayout"
|
||||
:virtualScrollerContentProps="virtualScrollerContentProps"
|
||||
:ariaControls="expandedRowId + '_' + index + '_expansion'"
|
||||
:name="nameAttributeSelector"
|
||||
@radio-change="onRadioChange($event)"
|
||||
@checkbox-change="onCheckboxChange($event)"
|
||||
@row-toggle="onRowToggle($event)"
|
||||
|
@ -52,13 +57,11 @@
|
|||
@row-edit-init="onRowEditInit($event)"
|
||||
@row-edit-save="onRowEditSave($event)"
|
||||
@row-edit-cancel="onRowEditCancel($event)"
|
||||
:editingMeta="editingMeta"
|
||||
@editing-meta-change="onEditingMetaChange"
|
||||
:virtualScrollerContentProps="virtualScrollerContentProps"
|
||||
/>
|
||||
</template>
|
||||
</tr>
|
||||
<tr v-if="templates['expansion'] && expandedRows && isRowExpanded(rowData)" :key="getRowKey(rowData, getRowIndex(index)) + '_expansion'" class="p-datatable-row-expansion" role="row">
|
||||
<tr v-if="templates['expansion'] && expandedRows && isRowExpanded(rowData)" :key="getRowKey(rowData, getRowIndex(index)) + '_expansion'" :id="expandedRowId + '_' + index + '_expansion'" class="p-datatable-row-expansion" role="row">
|
||||
<td :colspan="columnsLength">
|
||||
<component :is="templates['expansion']" :data="rowData" :index="getRowIndex(index)" />
|
||||
</td>
|
||||
|
@ -77,7 +80,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ObjectUtils, DomHandler } from 'primevue/utils';
|
||||
import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import BodyCell from './BodyCell.vue';
|
||||
|
||||
export default {
|
||||
|
@ -128,7 +131,7 @@ export default {
|
|||
default: null
|
||||
},
|
||||
groupRowsBy: {
|
||||
type: [Array, String],
|
||||
type: [Array, String, Function],
|
||||
default: null
|
||||
},
|
||||
expandableRowGroups: {
|
||||
|
@ -230,7 +233,9 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
rowGroupHeaderStyleObject: {}
|
||||
rowGroupHeaderStyleObject: {},
|
||||
tabindexArray: [],
|
||||
isARowSelected: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -548,6 +553,13 @@ export default {
|
|||
const contentRef = this.getVirtualScrollerProp('contentRef');
|
||||
|
||||
contentRef && contentRef(el);
|
||||
},
|
||||
setRowTabindex(index) {
|
||||
if (this.selection === null && (this.selectionMode === 'single' || this.selectionMode === 'multiple')) {
|
||||
return index === 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -569,6 +581,12 @@ export default {
|
|||
},
|
||||
bodyStyle() {
|
||||
return this.getVirtualScrollerProp('contentStyle');
|
||||
},
|
||||
expandedRowId() {
|
||||
return UniqueComponentId();
|
||||
},
|
||||
nameAttributeSelector() {
|
||||
return UniqueComponentId();
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import FooterCell from './FooterCell.vue';
|
||||
import { ObjectUtils } from 'primevue/utils';
|
||||
import FooterCell from './FooterCell.vue';
|
||||
|
||||
export default {
|
||||
name: 'TableFooter',
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
:filters="filters"
|
||||
:filterDisplay="filterDisplay"
|
||||
:filtersStore="filtersStore"
|
||||
:filterInputProps="filterInputProps"
|
||||
@filter-change="$emit('filter-change', $event)"
|
||||
@filter-apply="$emit('filter-apply')"
|
||||
@operator-change="$emit('operator-change', $event)"
|
||||
|
@ -40,7 +41,7 @@
|
|||
<tr v-if="filterDisplay === 'row'" role="row">
|
||||
<template v-for="(col, i) of columns" :key="columnProp(col, 'columnKey') || columnProp(col, 'field') || i">
|
||||
<th v-if="!columnProp(col, 'hidden') && (rowGroupMode !== 'subheader' || groupRowsBy !== columnProp(col, 'field'))" :style="getFilterColumnHeaderStyle(col)" :class="getFilterColumnHeaderClass(col)">
|
||||
<DTHeaderCheckbox v-if="columnProp(col, 'selectionMode') === 'multiple'" :checked="allRowsSelected" @change="$emit('checkbox-change', $event)" :disabled="empty" />
|
||||
<DTHeaderCheckbox v-if="columnProp(col, 'selectionMode') === 'multiple'" :checked="allRowsSelected" :disabled="empty" @change="$emit('checkbox-change', $event)" />
|
||||
<DTColumnFilter
|
||||
v-if="col.children && col.children.filter"
|
||||
:field="columnProp(col, 'filterField') || columnProp(col, 'field')"
|
||||
|
@ -54,6 +55,7 @@
|
|||
:filterApplyTemplate="col.children && col.children.filterapply"
|
||||
:filters="filters"
|
||||
:filtersStore="filtersStore"
|
||||
:filterInputProps="filterInputProps"
|
||||
@filter-change="$emit('filter-change', $event)"
|
||||
@filter-apply="$emit('filter-apply')"
|
||||
:filterMenuStyle="columnProp(col, 'filterMenuStyle')"
|
||||
|
@ -110,10 +112,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ObjectUtils } from 'primevue/utils';
|
||||
import ColumnFilter from './ColumnFilter.vue';
|
||||
import HeaderCell from './HeaderCell.vue';
|
||||
import HeaderCheckbox from './HeaderCheckbox.vue';
|
||||
import ColumnFilter from './ColumnFilter.vue';
|
||||
import { ObjectUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'TableHeader',
|
||||
|
@ -149,7 +151,7 @@ export default {
|
|||
default: null
|
||||
},
|
||||
groupRowsBy: {
|
||||
type: [Array, String],
|
||||
type: [Array, String, Function],
|
||||
default: null
|
||||
},
|
||||
resizableColumns: {
|
||||
|
@ -199,6 +201,10 @@ export default {
|
|||
reorderableColumns: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filterInputProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<tbody class="p-datatable-tbody">
|
||||
<tr v-for="n in rows" :key="n">
|
||||
<tr v-for="n in rows" :key="n" role="row">
|
||||
<td v-for="(col, i) of columns" :key="col.props.columnKey || col.props.field || i">
|
||||
<component v-if="col.children && col.children.loading" :is="col.children.loading" :column="col" :index="i" />
|
||||
</td>
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import DataView from './DataView.vue';
|
||||
|
||||
describe('DataView.vue', () => {
|
||||
it('should exist', () => {
|
||||
const wrapper = mount(DataView, {
|
||||
global: {
|
||||
plugins: [PrimeVue]
|
||||
},
|
||||
props: {
|
||||
value: [
|
||||
{
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { config, mount } from '@vue/test-utils';
|
||||
import DataViewLayoutOptions from './DataViewLayoutOptions.vue';
|
||||
|
||||
config.global.mocks = {
|
||||
$primevue: {
|
||||
config: {
|
||||
locale: {
|
||||
aria: {
|
||||
listView: 'listView',
|
||||
gridView: 'gridView'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('DataViewLayoutOptions.vue', () => {
|
||||
it('should exist', async () => {
|
||||
const wrapper = mount(DataViewLayoutOptions, {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="p-dataview-layout-options p-selectbutton p-buttonset">
|
||||
<button :class="buttonListClass" @click="changeLayout('list')" type="button">
|
||||
<div class="p-dataview-layout-options p-selectbutton p-buttonset" role="group">
|
||||
<button :aria-label="listViewAriaLabel" :class="buttonListClass" @click="changeLayout('list')" type="button" :aria-pressed="isListButtonPressed">
|
||||
<i class="pi pi-bars"></i>
|
||||
</button>
|
||||
<button :class="buttonGridClass" @click="changeLayout('grid')" type="button">
|
||||
<button :aria-label="gridViewAriaLabel" :class="buttonGridClass" @click="changeLayout('grid')" type="button" :aria-pressed="isGridButtonPressed">
|
||||
<i class="pi pi-th-large"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -16,9 +16,23 @@ export default {
|
|||
props: {
|
||||
modelValue: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isListButtonPressed: false,
|
||||
isGridButtonPressed: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
changeLayout(layout) {
|
||||
this.$emit('update:modelValue', layout);
|
||||
|
||||
if (layout === 'list') {
|
||||
this.isListButtonPressed = true;
|
||||
this.isGridButtonPressed = false;
|
||||
} else if (layout === 'grid') {
|
||||
this.isGridButtonPressed = true;
|
||||
this.isListButtonPressed = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -27,6 +41,12 @@ export default {
|
|||
},
|
||||
buttonGridClass() {
|
||||
return ['p-button p-button-icon-only', { 'p-highlight': this.modelValue === 'grid' }];
|
||||
},
|
||||
listViewAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.listView : undefined;
|
||||
},
|
||||
gridViewAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.gridView : undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VNode } from 'vue';
|
||||
import { HTMLAttributes, VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
type DialogPositionType = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'topleft' | 'topright' | 'bottomleft' | 'bottomright' | undefined;
|
||||
|
@ -49,6 +49,10 @@ export interface DialogProps {
|
|||
* Style class of the content section.
|
||||
*/
|
||||
contentClass?: any;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLDivElement to the overlay panel inside the component.
|
||||
*/
|
||||
contentProps?: HTMLAttributes | undefined;
|
||||
/**
|
||||
* When enabled dialog is displayed in RTL direction.
|
||||
*/
|
||||
|
@ -82,11 +86,6 @@ export interface DialogProps {
|
|||
* Default value is true.
|
||||
*/
|
||||
autoZIndex?: boolean | undefined;
|
||||
/**
|
||||
* Aria label of the close icon.
|
||||
* Default value is 'close'.
|
||||
*/
|
||||
ariaCloseLabel?: string | undefined;
|
||||
/**
|
||||
* Position of the dialog, options are 'center', 'top', 'bottom', 'left', 'right', 'topleft', 'topright', 'bottomleft' or 'bottomright'.
|
||||
* @see DialogPositionType
|
||||
|
@ -132,6 +131,21 @@ export interface DialogProps {
|
|||
* Style of the dynamic dialog.
|
||||
*/
|
||||
style?: any;
|
||||
/**
|
||||
* Icon to display in the dialog close button.
|
||||
* Default value is 'pi pi-times'.
|
||||
*/
|
||||
closeIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in the dialog maximize button when dialog is not maximized.
|
||||
* Default value is 'pi pi-window-maximize'.
|
||||
*/
|
||||
maximizeIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in the dialog maximize button when dialog is maximized.
|
||||
* Default value is 'pi pi-window-minimize'.
|
||||
*/
|
||||
minimizeIcon?: string | undefined;
|
||||
}
|
||||
|
||||
export interface DialogSlots {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Dialog from './Dialog.vue';
|
||||
|
||||
|
@ -8,11 +8,17 @@ describe('Dialog.vue', () => {
|
|||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
teleport: true,
|
||||
transition: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
visible: false
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerVisible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -31,16 +37,86 @@ describe('Dialog.vue', () => {
|
|||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
visible: true
|
||||
},
|
||||
slots: {
|
||||
default: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit</p>',
|
||||
footer: '<p>Dialog Footer</p>'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerVisible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
expect(wrapper.find('.p-dialog-content').exists()).toBe(false);
|
||||
expect(wrapper.find('.p-dialog-footer').exists()).toBe(false);
|
||||
await wrapper.setProps({ visible: true });
|
||||
|
||||
expect(wrapper.find('.p-dialog-content').exists()).toBe(true);
|
||||
expect(wrapper.find('.p-dialog-footer').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closable', () => {
|
||||
it('should have custom close icon when provided', async () => {
|
||||
const wrapper = mount(Dialog, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
closable: true,
|
||||
closeIcon: 'pi pi-discord',
|
||||
showHeader: true,
|
||||
visible: false
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerVisible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.setProps({ visible: true });
|
||||
|
||||
const icon = wrapper.find('.p-dialog-header-close-icon');
|
||||
|
||||
expect(icon.classes()).toContain('pi-discord');
|
||||
});
|
||||
});
|
||||
|
||||
describe('maximizable', () => {
|
||||
it('should have custom maximize and minimize icons when provided', async () => {
|
||||
const wrapper = mount(Dialog, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
maximizable: true,
|
||||
maximizeIcon: 'pi pi-discord',
|
||||
minimizeIcon: 'pi pi-facebook',
|
||||
showHeader: true,
|
||||
visible: false
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
containerVisible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.setProps({ visible: true });
|
||||
await wrapper.setData({ maximized: false });
|
||||
|
||||
const icon = wrapper.find('.p-dialog-header-maximize-icon');
|
||||
|
||||
expect(icon.classes()).toContain('pi-discord');
|
||||
|
||||
await wrapper.setData({ maximized: true });
|
||||
|
||||
expect(icon.classes()).toContain('pi-facebook');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
<Portal :appendTo="appendTo">
|
||||
<div v-if="containerVisible" :ref="maskRef" :class="maskClass" @click="onMaskClick">
|
||||
<transition name="p-dialog" @before-enter="onBeforeEnter" @enter="onEnter" @before-leave="onBeforeLeave" @leave="onLeave" @after-leave="onAfterLeave" appear>
|
||||
<div v-if="visible" :ref="containerRef" :class="dialogClass" v-bind="$attrs" role="dialog" :aria-labelledby="ariaLabelledById" :aria-modal="modal">
|
||||
<div v-if="showHeader" class="p-dialog-header" @mousedown="initDrag">
|
||||
<div v-if="visible" :ref="containerRef" v-focustrap="{ disabled: !modal }" :class="dialogClass" role="dialog" :aria-labelledby="ariaLabelledById" :aria-modal="modal" v-bind="$attrs">
|
||||
<div v-if="showHeader" :ref="headerContainerRef" class="p-dialog-header" @mousedown="initDrag">
|
||||
<slot name="header">
|
||||
<span v-if="header" :id="ariaLabelledById" class="p-dialog-title">{{ header }}</span>
|
||||
</slot>
|
||||
<div class="p-dialog-header-icons">
|
||||
<button v-if="maximizable" v-ripple class="p-dialog-header-icon p-dialog-header-maximize p-link" @click="maximize" type="button" tabindex="-1">
|
||||
<button v-if="maximizable" :ref="maximizableRef" v-ripple :autofocus="focusable" class="p-dialog-header-icon p-dialog-header-maximize p-link" @click="maximize" type="button" :tabindex="maximizable ? '0' : '-1'">
|
||||
<span :class="maximizeIconClass"></span>
|
||||
</button>
|
||||
<button v-if="closable" v-ripple class="p-dialog-header-icon p-dialog-header-close p-link" @click="close" :aria-label="ariaCloseLabel" type="button">
|
||||
<span class="p-dialog-header-close-icon pi pi-times"></span>
|
||||
<button v-if="closable" :ref="closeButtonRef" v-ripple :autofocus="focusable" class="p-dialog-header-icon p-dialog-header-close p-link" @click="close" :aria-label="closeAriaLabel" type="button" v-bind="closeButtonProps">
|
||||
<span :class="['p-dialog-header-close-icon', closeIcon]"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="contentStyleClass" :style="contentStyle">
|
||||
<div :ref="contentRef" :class="contentStyleClass" :style="contentStyle" v-bind="contentProps">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="footer || $slots.footer" class="p-dialog-footer">
|
||||
<div v-if="footer || $slots.footer" :ref="footerContainerRef" class="p-dialog-footer">
|
||||
<slot name="footer">{{ footer }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,25 +29,57 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue';
|
||||
import { UniqueComponentId, DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import FocusTrap from 'primevue/focustrap';
|
||||
import Portal from 'primevue/portal';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'Dialog',
|
||||
inheritAttrs: false,
|
||||
emits: ['update:visible', 'show', 'hide', 'after-hide', 'maximize', 'unmaximize', 'dragend'],
|
||||
props: {
|
||||
header: null,
|
||||
footer: null,
|
||||
visible: Boolean,
|
||||
modal: Boolean,
|
||||
contentStyle: null,
|
||||
contentClass: String,
|
||||
rtl: Boolean,
|
||||
maximizable: Boolean,
|
||||
dismissableMask: Boolean,
|
||||
header: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
footer: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
contentStyle: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
contentClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
contentProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
rtl: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
maximizable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dismissableMask: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
|
@ -68,10 +100,6 @@ export default {
|
|||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
ariaCloseLabel: {
|
||||
type: String,
|
||||
default: 'close'
|
||||
},
|
||||
position: {
|
||||
type: String,
|
||||
default: 'center'
|
||||
|
@ -100,6 +128,22 @@ export default {
|
|||
type: String,
|
||||
default: 'body'
|
||||
},
|
||||
closeIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-times'
|
||||
},
|
||||
maximizeIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-window-maximize'
|
||||
},
|
||||
minimizeIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-window-minimize'
|
||||
},
|
||||
closeButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
_instance: null
|
||||
},
|
||||
provide() {
|
||||
|
@ -110,12 +154,18 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
containerVisible: this.visible,
|
||||
maximized: false
|
||||
maximized: false,
|
||||
focusable: false
|
||||
};
|
||||
},
|
||||
documentKeydownListener: null,
|
||||
container: null,
|
||||
mask: null,
|
||||
content: null,
|
||||
headerContainer: null,
|
||||
footerContainer: null,
|
||||
maximizableButton: null,
|
||||
closeButton: null,
|
||||
styleElement: null,
|
||||
dragging: null,
|
||||
documentDragListener: null,
|
||||
|
@ -168,6 +218,7 @@ export default {
|
|||
},
|
||||
onLeave() {
|
||||
this.$emit('hide');
|
||||
this.focusable = false;
|
||||
},
|
||||
onAfterLeave() {
|
||||
if (this.autoZIndex) {
|
||||
|
@ -185,9 +236,26 @@ export default {
|
|||
}
|
||||
},
|
||||
focus() {
|
||||
let focusTarget = this.container.querySelector('[autofocus]');
|
||||
const findFocusableElement = (container) => {
|
||||
return container.querySelector('[autofocus]');
|
||||
};
|
||||
|
||||
let focusTarget = this.$slots.footer && findFocusableElement(this.footerContainer);
|
||||
|
||||
if (!focusTarget) {
|
||||
focusTarget = this.$slots.header && findFocusableElement(this.headerContainer);
|
||||
|
||||
if (!focusTarget) {
|
||||
focusTarget = this.$slots.default && findFocusableElement(this.content);
|
||||
|
||||
if (!focusTarget) {
|
||||
focusTarget = findFocusableElement(this.container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (focusTarget) {
|
||||
this.focusable = true;
|
||||
focusTarget.focus();
|
||||
}
|
||||
},
|
||||
|
@ -216,26 +284,7 @@ export default {
|
|||
}
|
||||
},
|
||||
onKeyDown(event) {
|
||||
if (event.which === 9) {
|
||||
event.preventDefault();
|
||||
let focusableElements = DomHandler.getFocusableElements(this.container);
|
||||
|
||||
if (focusableElements && focusableElements.length > 0) {
|
||||
if (!document.activeElement) {
|
||||
focusableElements[0].focus();
|
||||
} else {
|
||||
let focusedIndex = focusableElements.indexOf(document.activeElement);
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (focusedIndex == -1 || focusedIndex === 0) focusableElements[focusableElements.length - 1].focus();
|
||||
else focusableElements[focusedIndex - 1].focus();
|
||||
} else {
|
||||
if (focusedIndex == -1 || focusedIndex === focusableElements.length - 1) focusableElements[0].focus();
|
||||
else focusableElements[focusedIndex + 1].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.which === 27 && this.closeOnEscape) {
|
||||
if (event.code === 'Escape' && this.closeOnEscape) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
@ -263,6 +312,21 @@ export default {
|
|||
maskRef(el) {
|
||||
this.mask = el;
|
||||
},
|
||||
contentRef(el) {
|
||||
this.content = el;
|
||||
},
|
||||
headerContainerRef(el) {
|
||||
this.headerContainer = el;
|
||||
},
|
||||
footerContainerRef(el) {
|
||||
this.footerContainer = el;
|
||||
},
|
||||
maximizableRef(el) {
|
||||
this.maximizableButton = el;
|
||||
},
|
||||
closeButtonRef(el) {
|
||||
this.closeButton = el;
|
||||
},
|
||||
createStyle() {
|
||||
if (!this.styleElement) {
|
||||
this.styleElement = document.createElement('style');
|
||||
|
@ -396,10 +460,10 @@ export default {
|
|||
},
|
||||
maximizeIconClass() {
|
||||
return [
|
||||
'p-dialog-header-maximize-icon pi',
|
||||
'p-dialog-header-maximize-icon',
|
||||
{
|
||||
'pi-window-maximize': !this.maximized,
|
||||
'pi-window-minimize': this.maximized
|
||||
[this.maximizeIcon]: !this.maximized,
|
||||
[this.minimizeIcon]: this.maximized
|
||||
}
|
||||
];
|
||||
},
|
||||
|
@ -407,7 +471,10 @@ export default {
|
|||
return UniqueComponentId();
|
||||
},
|
||||
ariaLabelledById() {
|
||||
return this.header != null ? this.ariaId + '_header' : null;
|
||||
return this.header != null || this.$attrs['aria-labelledby'] !== null ? this.ariaId + '_header' : null;
|
||||
},
|
||||
closeAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.close : undefined;
|
||||
},
|
||||
attributeSelector() {
|
||||
return UniqueComponentId();
|
||||
|
@ -417,7 +484,8 @@ export default {
|
|||
}
|
||||
},
|
||||
directives: {
|
||||
ripple: Ripple
|
||||
ripple: Ripple,
|
||||
focustrap: FocusTrap
|
||||
},
|
||||
components: {
|
||||
Portal: Portal
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
import { MenuItem } from '../menuitem';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
type DockPositionType = 'bottom' | 'top' | 'left' | 'right' | undefined;
|
||||
|
||||
|
@ -53,6 +53,22 @@ export interface DockProps {
|
|||
* @see DockTooltipOptions
|
||||
*/
|
||||
tooltipOptions?: DockTooltipOptions;
|
||||
/**
|
||||
* Unique identifier of the menu.
|
||||
*/
|
||||
menuId?: string | undefined;
|
||||
/**
|
||||
* Index of the element in tabbing order.
|
||||
*/
|
||||
tabindex?: number | string | undefined;
|
||||
/**
|
||||
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
|
||||
*/
|
||||
'aria-labelledby'?: string | undefined;
|
||||
/**
|
||||
* Establishes a string value that labels the component.
|
||||
*/
|
||||
'aria-label'?: string | undefined;
|
||||
}
|
||||
|
||||
export interface DockSlots {
|
||||
|
@ -65,6 +81,10 @@ export interface DockSlots {
|
|||
* Custom content for item.
|
||||
*/
|
||||
item: MenuItem;
|
||||
/**
|
||||
* Index of the menuitem
|
||||
*/
|
||||
index: number;
|
||||
}) => VNode[];
|
||||
/**
|
||||
* Custom icon content.
|
||||
|
@ -78,7 +98,18 @@ export interface DockSlots {
|
|||
}) => VNode[];
|
||||
}
|
||||
|
||||
export declare type DockEmits = {};
|
||||
export declare type DockEmits = {
|
||||
/**
|
||||
* Callback to invoke when the component receives focus.
|
||||
* @param {Event} event - Browser event.
|
||||
*/
|
||||
focus: (event: Event) => void;
|
||||
/**
|
||||
* Callback to invoke when the component loses focus.
|
||||
* @param {Event} event - Browser event.
|
||||
*/
|
||||
blur: (event: Event) => void;
|
||||
};
|
||||
|
||||
declare class Dock extends ClassComponent<DockProps, DockSlots, DockEmits> {}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div :class="containerClass" :style="style">
|
||||
<DockSub :model="model" :templates="$slots" :exact="exact" :tooltipOptions="tooltipOptions"></DockSub>
|
||||
<DockSub :model="model" :templates="$slots" :exact="exact" :tooltipOptions="tooltipOptions" :position="position" :menuId="menuId" :aria-label="ariaLabel" :aria-labelledby="ariaLabelledby" :tabindex="tabindex"></DockSub>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -21,6 +21,22 @@ export default {
|
|||
exact: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
menuId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
'aria-label': {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
'aria-labelledby': {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -63,7 +79,7 @@ export default {
|
|||
will-change: transform;
|
||||
}
|
||||
|
||||
.p-dock-action {
|
||||
.p-dock-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,41 +1,59 @@
|
|||
<template>
|
||||
<div class="p-dock-list-container">
|
||||
<ul ref="list" class="p-dock-list" role="menu" @mouseleave="onListMouseLeave">
|
||||
<li v-for="(item, index) of model" :key="index" :class="itemClass(index)" role="none" @mouseenter="onItemMouseEnter(index)">
|
||||
<template v-if="!templates['item']">
|
||||
<router-link v-if="item.to && !disabled(item)" v-slot="{ navigate, href, isActive, isExactActive }" :to="item.to" custom>
|
||||
<a
|
||||
v-tooltip:[tooltipOptions]="{ value: item.label, disabled: !tooltipOptions }"
|
||||
:href="href"
|
||||
role="menuitem"
|
||||
:class="linkClass(item, { isActive, isExactActive })"
|
||||
:target="item.target"
|
||||
@click="onItemClick($event, item, navigate)"
|
||||
>
|
||||
<template v-if="!templates['icon']">
|
||||
<span v-ripple :class="['p-dock-action-icon', item.icon]"></span>
|
||||
</template>
|
||||
<component v-else :is="templates['icon']" :item="item"></component>
|
||||
</a>
|
||||
</router-link>
|
||||
<a
|
||||
v-else
|
||||
v-tooltip:[tooltipOptions]="{ value: item.label, disabled: !tooltipOptions }"
|
||||
:href="item.url"
|
||||
role="menuitem"
|
||||
:class="linkClass(item)"
|
||||
:target="item.target"
|
||||
@click="onItemClick($event, item)"
|
||||
:tabindex="disabled(item) ? null : '0'"
|
||||
>
|
||||
<template v-if="!templates['icon']">
|
||||
<span v-ripple :class="['p-dock-action-icon', item.icon]"></span>
|
||||
<ul
|
||||
ref="list"
|
||||
:id="id"
|
||||
class="p-dock-list"
|
||||
role="menu"
|
||||
:aria-orientation="position === 'bottom' || position === 'top' ? 'horizontal' : 'vertical'"
|
||||
:aria-activedescendant="focused ? focusedOptionId : undefined"
|
||||
:tabindex="tabindex"
|
||||
:aria-label="ariaLabel"
|
||||
:aria-labelledby="ariaLabelledby"
|
||||
@focus="onListFocus"
|
||||
@blur="onListBlur"
|
||||
@keydown="onListKeyDown"
|
||||
@mouseleave="onListMouseLeave"
|
||||
>
|
||||
<template v-for="(processedItem, index) of model" :key="index">
|
||||
<li
|
||||
:id="getItemId(index)"
|
||||
:class="itemClass(processedItem, index, getItemId(index))"
|
||||
role="menuitem"
|
||||
:aria-label="processedItem.label"
|
||||
:aria-disabled="disabled(processedItem)"
|
||||
@click="onItemClick($event, processedItem)"
|
||||
@mouseenter="onItemMouseEnter(index)"
|
||||
>
|
||||
<div class="p-menuitem-content">
|
||||
<template v-if="!templates['item']">
|
||||
<router-link v-if="processedItem.to && !disabled(processedItem)" v-slot="{ navigate, href, isActive, isExactActive }" :to="processedItem.to" custom>
|
||||
<a
|
||||
v-tooltip:[tooltipOptions]="{ value: processedItem.label, disabled: !tooltipOptions }"
|
||||
:href="href"
|
||||
:class="linkClass({ isActive, isExactActive })"
|
||||
:target="processedItem.target"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
@click="onItemActionClick($event, processedItem, navigate)"
|
||||
>
|
||||
<template v-if="!templates['icon']">
|
||||
<span v-ripple :class="['p-dock-icon', processedItem.icon]"></span>
|
||||
</template>
|
||||
<component v-else :is="templates['icon']" :item="processedItem"></component>
|
||||
</a>
|
||||
</router-link>
|
||||
<a v-else v-tooltip:[tooltipOptions]="{ value: processedItem.label, disabled: !tooltipOptions }" :href="processedItem.url" :class="linkClass()" :target="processedItem.target" tabindex="-1" aria-hidden="true">
|
||||
<template v-if="!templates['icon']">
|
||||
<span v-ripple :class="['p-dock-icon', processedItem.icon]"></span>
|
||||
</template>
|
||||
<component v-else :is="templates['icon']" :item="processedItem"></component>
|
||||
</a>
|
||||
</template>
|
||||
<component v-else :is="templates['icon']" :item="item"></component>
|
||||
</a>
|
||||
</template>
|
||||
<component v-else :is="templates['item']" :item="item"></component>
|
||||
</li>
|
||||
<component v-else :is="templates['item']" :item="processedItem" :index="index"></component>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -43,10 +61,16 @@
|
|||
<script>
|
||||
import Ripple from 'primevue/ripple';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'DockSub',
|
||||
emits: ['focus', 'blur'],
|
||||
props: {
|
||||
position: {
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
},
|
||||
model: {
|
||||
type: Array,
|
||||
default: null
|
||||
|
@ -59,42 +83,164 @@ export default {
|
|||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
tooltipOptions: null
|
||||
tooltipOptions: null,
|
||||
menuId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
'aria-label': {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
'aria-labelledby': {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentIndex: -3
|
||||
currentIndex: -3,
|
||||
focused: false,
|
||||
focusedOptionIndex: -1
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getItemId(index) {
|
||||
return `${this.id}_${index}`;
|
||||
},
|
||||
getItemProp(processedItem, name) {
|
||||
return processedItem && processedItem.item ? ObjectUtils.getItemValue(processedItem.item[name]) : undefined;
|
||||
},
|
||||
isSameMenuItem(event) {
|
||||
return event.currentTarget && (event.currentTarget.isSameNode(event.target) || event.currentTarget.isSameNode(event.target.closest('.p-menuitem')));
|
||||
},
|
||||
onListMouseLeave() {
|
||||
this.currentIndex = -3;
|
||||
},
|
||||
onItemMouseEnter(index) {
|
||||
this.currentIndex = index;
|
||||
},
|
||||
onItemClick(event, item, navigate) {
|
||||
if (this.disabled(item)) {
|
||||
event.preventDefault();
|
||||
onItemActionClick(event, navigate) {
|
||||
navigate && navigate(event);
|
||||
},
|
||||
onItemClick(event, processedItem) {
|
||||
if (this.isSameMenuItem(event)) {
|
||||
const command = this.getItemProp(processedItem, 'command');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.command) {
|
||||
item.command({
|
||||
originalEvent: event,
|
||||
item: item
|
||||
});
|
||||
}
|
||||
|
||||
if (item.to && navigate) {
|
||||
navigate(event);
|
||||
command && command({ originalEvent: event, item: processedItem.item });
|
||||
}
|
||||
},
|
||||
itemClass(index) {
|
||||
onListFocus(event) {
|
||||
this.focused = true;
|
||||
this.changeFocusedOptionIndex(0);
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
onListBlur(event) {
|
||||
this.focused = false;
|
||||
this.focusedOptionIndex = -1;
|
||||
this.$emit('blur', event);
|
||||
},
|
||||
onListKeyDown(event) {
|
||||
switch (event.code) {
|
||||
case 'ArrowDown': {
|
||||
if (this.position === 'left' || this.position === 'right') this.onArrowDownKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ArrowUp': {
|
||||
if (this.position === 'left' || this.position === 'right') this.onArrowUpKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ArrowRight': {
|
||||
if (this.position === 'top' || this.position === 'bottom') this.onArrowDownKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ArrowLeft': {
|
||||
if (this.position === 'top' || this.position === 'bottom') this.onArrowUpKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Home': {
|
||||
this.onHomeKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'End': {
|
||||
this.onEndKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Enter':
|
||||
|
||||
case 'Space': {
|
||||
this.onSpaceKey(event);
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onArrowDownKey() {
|
||||
const optionIndex = this.findNextOptionIndex(this.focusedOptionIndex);
|
||||
|
||||
this.changeFocusedOptionIndex(optionIndex);
|
||||
},
|
||||
onArrowUpKey() {
|
||||
const optionIndex = this.findPrevOptionIndex(this.focusedOptionIndex);
|
||||
|
||||
this.changeFocusedOptionIndex(optionIndex);
|
||||
},
|
||||
onHomeKey() {
|
||||
this.changeFocusedOptionIndex(0);
|
||||
},
|
||||
onEndKey() {
|
||||
this.changeFocusedOptionIndex(DomHandler.find(this.$refs.list, 'li.p-dock-item:not(.p-disabled)').length - 1);
|
||||
},
|
||||
onSpaceKey() {
|
||||
const element = DomHandler.findSingle(this.$refs.list, `li[id="${`${this.focusedOptionIndex}`}"]`);
|
||||
const anchorElement = element && DomHandler.findSingle(element, '.p-dock-link');
|
||||
|
||||
anchorElement ? anchorElement.click() : element && element.click();
|
||||
},
|
||||
findNextOptionIndex(index) {
|
||||
const menuitems = DomHandler.find(this.$refs.list, 'li.p-dock-item:not(.p-disabled)');
|
||||
const matchedOptionIndex = [...menuitems].findIndex((link) => link.id === index);
|
||||
|
||||
return matchedOptionIndex > -1 ? matchedOptionIndex + 1 : 0;
|
||||
},
|
||||
findPrevOptionIndex(index) {
|
||||
const menuitems = DomHandler.find(this.$refs.list, 'li.p-dock-item:not(.p-disabled)');
|
||||
const matchedOptionIndex = [...menuitems].findIndex((link) => link.id === index);
|
||||
|
||||
return matchedOptionIndex > -1 ? matchedOptionIndex - 1 : 0;
|
||||
},
|
||||
changeFocusedOptionIndex(index) {
|
||||
const menuitems = DomHandler.find(this.$refs.list, 'li.p-dock-item:not(.p-disabled)');
|
||||
|
||||
let order = index >= menuitems.length ? menuitems.length - 1 : index < 0 ? 0 : index;
|
||||
|
||||
this.focusedOptionIndex = menuitems[order].getAttribute('id');
|
||||
},
|
||||
itemClass(item, index, id) {
|
||||
return [
|
||||
'p-dock-item',
|
||||
{
|
||||
'p-focus': id === this.focusedOptionIndex,
|
||||
'p-disabled': this.disabled(item),
|
||||
'p-dock-item-second-prev': this.currentIndex - 2 === index,
|
||||
'p-dock-item-prev': this.currentIndex - 1 === index,
|
||||
'p-dock-item-current': this.currentIndex === index,
|
||||
|
@ -103,11 +249,10 @@ export default {
|
|||
}
|
||||
];
|
||||
},
|
||||
linkClass(item, routerProps) {
|
||||
linkClass(routerProps) {
|
||||
return [
|
||||
'p-dock-action',
|
||||
'p-dock-link',
|
||||
{
|
||||
'p-disabled': this.disabled(item),
|
||||
'router-link-active': routerProps && routerProps.isActive,
|
||||
'router-link-active-exact': this.exact && routerProps && routerProps.isExactActive
|
||||
}
|
||||
|
@ -117,6 +262,14 @@ export default {
|
|||
return typeof item.disabled === 'function' ? item.disabled() : item.disabled;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return this.menuId || UniqueComponentId();
|
||||
},
|
||||
focusedOptionId() {
|
||||
return this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : null;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
ripple: Ripple,
|
||||
tooltip: Tooltip
|
||||
|
|
|
@ -163,6 +163,21 @@ export interface DropdownProps {
|
|||
* Whether the dropdown is in loading state.
|
||||
*/
|
||||
loading?: boolean | undefined;
|
||||
/**
|
||||
* Icon to display in clear button.
|
||||
* Default value is 'pi pi-times'.
|
||||
*/
|
||||
clearIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in the dropdown.
|
||||
* Default value is 'pi pi-chevron-down'.
|
||||
*/
|
||||
dropdownIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in filter input.
|
||||
* Default value is 'pi pi-search'.
|
||||
*/
|
||||
filterIcon?: string | undefined;
|
||||
/**
|
||||
* Icon to display in loading state.
|
||||
* Default value is 'pi pi-spinner pi-spin'.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from '../config/PrimeVue';
|
||||
import Dropdown from '../dropdown/Dropdown.vue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Dropdown from '@/components/dropdown/Dropdown.vue';
|
||||
|
||||
describe('Dropdown.vue', () => {
|
||||
let wrapper;
|
||||
|
@ -22,10 +22,9 @@ describe('Dropdown.vue', () => {
|
|||
it('should Dropdown exist', () => {
|
||||
expect(wrapper.find('.p-dropdown.p-component').exists()).toBe(true);
|
||||
expect(wrapper.find('.p-dropdown-panel').exists()).toBe(true);
|
||||
|
||||
expect(wrapper.find('.p-focus').exists()).toBe(false);
|
||||
expect(wrapper.find('.p-dropdown-empty-message').exists()).toBe(true);
|
||||
expect(wrapper.find('.p-focus').exists()).toBe(true);
|
||||
expect(wrapper.find('.p-inputwrapper-filled').exists()).toBe(false);
|
||||
|
||||
expect(wrapper.find('.p-inputwrapper-focus').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -67,6 +66,32 @@ describe('option checks', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('clear checks', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(Dropdown, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
clearIcon: 'pi pi-discord',
|
||||
modelValue: 'value',
|
||||
showClear: true
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.trigger('click');
|
||||
});
|
||||
|
||||
it('should have correct icon', () => {
|
||||
expect(wrapper.find('.p-dropdown-clear-icon').classes()).toContain('pi-discord');
|
||||
});
|
||||
});
|
||||
|
||||
describe('editable checks', () => {
|
||||
let wrapper;
|
||||
|
||||
|
@ -295,6 +320,7 @@ describe('filter checks', () => {
|
|||
},
|
||||
props: {
|
||||
filter: true,
|
||||
filterIcon: 'pi pi-discord',
|
||||
options: [
|
||||
{ name: 'Australia', code: 'AU' },
|
||||
{ name: 'Brazil', code: 'BR' },
|
||||
|
@ -316,11 +342,13 @@ describe('filter checks', () => {
|
|||
|
||||
it('should make filtering', async () => {
|
||||
const filterInput = wrapper.find('.p-dropdown-filter');
|
||||
const filterIcon = wrapper.find('.p-dropdown-filter-icon');
|
||||
|
||||
expect(filterInput.exists()).toBe(true);
|
||||
expect(filterIcon.classes()).toContain('pi-discord');
|
||||
|
||||
const event = { target: { value: 'c' } };
|
||||
const onFilterChange = vi.spyOn(wrapper.vm, 'onFilterChange');
|
||||
const onFilterChange = jest.spyOn(wrapper.vm, 'onFilterChange');
|
||||
|
||||
wrapper.vm.onFilterChange(event);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
>
|
||||
<slot name="value" :value="modelValue" :placeholder="placeholder">{{ label === 'p-emptylabel' ? ' ' : label || 'empty' }}</slot>
|
||||
</span>
|
||||
<i v-if="showClear && modelValue != null" class="p-dropdown-clear-icon pi pi-times" @click="onClearClick" v-bind="clearIconProps"></i>
|
||||
<i v-if="showClear && modelValue != null" :class="['p-dropdown-clear-icon', clearIcon]" @click="onClearClick" v-bind="clearIconProps"></i>
|
||||
<div class="p-dropdown-trigger">
|
||||
<slot name="indicator">
|
||||
<span :class="dropdownIconClass" aria-hidden="true"></span>
|
||||
|
@ -76,7 +76,7 @@
|
|||
@input="onFilterChange"
|
||||
v-bind="filterInputProps"
|
||||
/>
|
||||
<span class="p-dropdown-filter-icon pi pi-search"></span>
|
||||
<span :class="['p-dropdown-filter-icon', filterIcon]" />
|
||||
</div>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ filterResultMessageText }}
|
||||
|
@ -115,12 +115,6 @@
|
|||
<slot name="empty">{{ emptyMessageText }}</slot>
|
||||
</li>
|
||||
</ul>
|
||||
<span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ emptyMessageText }}
|
||||
</span>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="$slots.loader" v-slot:loader="{ options }">
|
||||
<slot name="loader" :options="options"></slot>
|
||||
|
@ -128,6 +122,12 @@
|
|||
</VirtualScroller>
|
||||
</div>
|
||||
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
|
||||
<span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ emptyMessageText }}
|
||||
</span>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
<span ref="lastHiddenFocusableElementOnOverlay" role="presentation" aria-hidden="true" class="p-hidden-accessible p-hidden-focusable" :tabindex="0" @focus="onLastHiddenFocus"></span>
|
||||
</div>
|
||||
</transition>
|
||||
|
@ -136,12 +136,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { ConnectedOverlayScrollHandler, ObjectUtils, DomHandler, ZIndexUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import { FilterService } from 'primevue/api';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import VirtualScroller from 'primevue/virtualscroller';
|
||||
import OverlayEventBus from 'primevue/overlayeventbus';
|
||||
import Portal from 'primevue/portal';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { ConnectedOverlayScrollHandler, DomHandler, ObjectUtils, UniqueComponentId, ZIndexUtils } from 'primevue/utils';
|
||||
import VirtualScroller from 'primevue/virtualscroller';
|
||||
|
||||
export default {
|
||||
name: 'Dropdown',
|
||||
|
@ -227,6 +227,18 @@ export default {
|
|||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-times'
|
||||
},
|
||||
dropdownIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-chevron-down'
|
||||
},
|
||||
filterIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-search'
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-spinner pi-spin'
|
||||
|
@ -386,7 +398,7 @@ export default {
|
|||
},
|
||||
onFocus(event) {
|
||||
this.focused = true;
|
||||
this.focusedOptionIndex = this.overlayVisible && this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1;
|
||||
this.focusedOptionIndex = this.focusedOptionIndex !== -1 ? this.focusedOptionIndex : this.overlayVisible && this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1;
|
||||
this.overlayVisible && this.scrollInView(this.focusedOptionIndex);
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
|
@ -434,6 +446,7 @@ export default {
|
|||
break;
|
||||
|
||||
case 'Enter':
|
||||
case 'NumpadEnter':
|
||||
this.onEnterKey(event);
|
||||
break;
|
||||
|
||||
|
@ -488,18 +501,14 @@ export default {
|
|||
this.updateModel(event, null);
|
||||
},
|
||||
onFirstHiddenFocus(event) {
|
||||
const relatedTarget = event.relatedTarget;
|
||||
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getFirstFocusableElement(this.overlay, ':not(.p-hidden-focusable)') : this.$refs.focusInput;
|
||||
|
||||
if (relatedTarget === this.$refs.focusInput) {
|
||||
const firstFocusableEl = DomHandler.getFirstFocusableElement(this.overlay, ':not(.p-hidden-focusable)');
|
||||
|
||||
DomHandler.focus(firstFocusableEl);
|
||||
} else {
|
||||
DomHandler.focus(this.$refs.focusInput);
|
||||
}
|
||||
DomHandler.focus(focusableEl);
|
||||
},
|
||||
onLastHiddenFocus() {
|
||||
DomHandler.focus(this.$refs.firstHiddenFocusableElementOnOverlay);
|
||||
onLastHiddenFocus(event) {
|
||||
const focusableEl = event.relatedTarget === this.$refs.focusInput ? DomHandler.getLastFocusableElement(this.overlay, ':not(.p-hidden-focusable)') : this.$refs.focusInput;
|
||||
|
||||
DomHandler.focus(focusableEl);
|
||||
},
|
||||
onOptionSelect(event, option, isHide = true) {
|
||||
const value = this.getOptionValue(option);
|
||||
|
@ -939,12 +948,31 @@ export default {
|
|||
];
|
||||
},
|
||||
dropdownIconClass() {
|
||||
return ['p-dropdown-trigger-icon', this.loading ? this.loadingIcon : 'pi pi-chevron-down'];
|
||||
return ['p-dropdown-trigger-icon', this.loading ? this.loadingIcon : this.dropdownIcon];
|
||||
},
|
||||
visibleOptions() {
|
||||
const options = this.optionGroupLabel ? this.flatOptions(this.options) : this.options || [];
|
||||
|
||||
return this.filterValue ? FilterService.filter(options, this.searchFields, this.filterValue, this.filterMatchMode, this.filterLocale) : options;
|
||||
if (this.filterValue) {
|
||||
const filteredOptions = FilterService.filter(options, this.searchFields, this.filterValue, this.filterMatchMode, this.filterLocale);
|
||||
|
||||
if (this.optionGroupLabel) {
|
||||
const optionGroups = this.options || [];
|
||||
const filtered = [];
|
||||
|
||||
optionGroups.forEach((group) => {
|
||||
const filteredItems = group.items.filter((item) => filteredOptions.includes(item));
|
||||
|
||||
if (filteredItems.length > 0) filtered.push({ ...group, items: [...filteredItems] });
|
||||
});
|
||||
|
||||
return this.flatOptions(filtered);
|
||||
}
|
||||
|
||||
return filteredOptions;
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
hasSelectedOption() {
|
||||
return ObjectUtils.isNotEmpty(this.modelValue);
|
||||
|
|
|
@ -28,7 +28,7 @@ export interface FieldsetProps {
|
|||
/**
|
||||
* Uses to pass the custom value to read for the anchor inside the component.
|
||||
*/
|
||||
toggleButtonProps?: string | undefined;
|
||||
toggleButtonProps?: object | undefined;
|
||||
}
|
||||
|
||||
export interface FieldsetSlots {
|
||||
|
|
|
@ -4,7 +4,19 @@
|
|||
<slot v-if="!toggleable" name="legend">
|
||||
<span :id="ariaId + '_header'" class="p-fieldset-legend-text">{{ legend }}</span>
|
||||
</slot>
|
||||
<a v-if="toggleable" :id="ariaId + '_header'" v-ripple tabindex="0" role="button" :aria-controls="ariaId + '_content'" :aria-expanded="!d_collapsed" :aria-label="toggleButtonProps || legend" @click="toggle" @keydown="onKeyDown">
|
||||
<a
|
||||
v-if="toggleable"
|
||||
:id="ariaId + '_header'"
|
||||
v-ripple
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-controls="ariaId + '_content'"
|
||||
:aria-expanded="!d_collapsed"
|
||||
:aria-label="buttonAriaLabel"
|
||||
@click="toggle"
|
||||
@keydown="onKeyDown"
|
||||
v-bind="toggleButtonProps"
|
||||
>
|
||||
<span :class="iconClass"></span>
|
||||
<slot name="legend">
|
||||
<span class="p-fieldset-legend-text">{{ legend }}</span>
|
||||
|
@ -22,8 +34,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { UniqueComponentId } from 'primevue/utils';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { UniqueComponentId } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'Fieldset',
|
||||
|
@ -32,7 +44,10 @@ export default {
|
|||
legend: String,
|
||||
toggleable: Boolean,
|
||||
collapsed: Boolean,
|
||||
toggleButtonProps: String
|
||||
toggleButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -72,6 +87,9 @@ export default {
|
|||
},
|
||||
ariaId() {
|
||||
return UniqueComponentId();
|
||||
},
|
||||
buttonAriaLabel() {
|
||||
return this.toggleButtonProps && this.toggleButtonProps['aria-label'] ? this.toggleButtonProps['aria-label'] : this.legend;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div v-for="(file, index) of files" :key="file.name + file.type + file.size" class="p-fileupload-file">
|
||||
<img role="presentation" class="p-fileupload-file-thumbnail" :alt="file.name" :src="file.objectURL" :width="previewWidth" />
|
||||
<div class="p-fileupload-file-details">
|
||||
<div class="p-fileupload-file-name">{{ file.name }}</div>
|
||||
<span class="p-fileupload-file-size">{{ formatSize(file.size) }}</span>
|
||||
<FileUploadBadge :value="badgeValue" class="p-fileupload-file-badge" :severity="badgeSeverity" />
|
||||
</div>
|
||||
<div class="p-fileupload-file-actions">
|
||||
<FileUploadButton icon="pi pi-times" @click="$emit('remove', index)" class="p-fileupload-file-remove p-button-text p-button-danger p-button-rounded"></FileUploadButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Badge from 'primevue/badge';
|
||||
import Button from 'primevue/button';
|
||||
|
||||
export default {
|
||||
emits: ['remove'],
|
||||
props: {
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
badgeSeverity: {
|
||||
type: String,
|
||||
default: 'warning'
|
||||
},
|
||||
badgeValue: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
previewWidth: {
|
||||
type: Number,
|
||||
default: 50
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatSize(bytes) {
|
||||
if (bytes === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
|
||||
let k = 1000,
|
||||
dm = 3,
|
||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FileUploadButton: Button,
|
||||
FileUploadBadge: Badge
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -87,6 +87,17 @@ export interface FileUploadRemoveEvent {
|
|||
files: File[];
|
||||
}
|
||||
|
||||
export interface FileUploadRemoveUploadedFile {
|
||||
/**
|
||||
* Removed file.
|
||||
*/
|
||||
file: File;
|
||||
/**
|
||||
* Remaining files to be uploaded.
|
||||
*/
|
||||
files: File[];
|
||||
}
|
||||
|
||||
export interface FileUploadProps {
|
||||
/**
|
||||
* Name of the request parameter to identify the files at backend.
|
||||
|
@ -202,7 +213,61 @@ export interface FileUploadProps {
|
|||
|
||||
export interface FileUploadSlots {
|
||||
/**
|
||||
* Custom empty template.
|
||||
* Custom header content template.
|
||||
*/
|
||||
header: (scope: {
|
||||
/**
|
||||
* Files to upload.
|
||||
*/
|
||||
files: File[];
|
||||
/**
|
||||
* Uploaded files.
|
||||
*/
|
||||
uploadedFiles: File[];
|
||||
/**
|
||||
* Choose function
|
||||
*/
|
||||
chooseCallback: () => void;
|
||||
/**
|
||||
* Upload function
|
||||
*/
|
||||
uploadCallback: () => void;
|
||||
/**
|
||||
* Clear function
|
||||
*/
|
||||
clearCallback: () => void;
|
||||
}) => VNode[];
|
||||
/**
|
||||
* Custom uploaded content template.
|
||||
*/
|
||||
content: (scope: {
|
||||
/**
|
||||
* Files to upload.
|
||||
*/
|
||||
files: File[];
|
||||
/**
|
||||
* Uploaded files.
|
||||
*/
|
||||
uploadedFiles: File[];
|
||||
/**
|
||||
* Function to remove an uploaded file.
|
||||
*/
|
||||
removeUploadedFileCallback: () => void;
|
||||
/**
|
||||
* Function to remove a file.
|
||||
*/
|
||||
removeFileCallback: () => void;
|
||||
/**
|
||||
* Uploaded progress as number.
|
||||
*/
|
||||
progress: number;
|
||||
/**
|
||||
* Status messages about upload process.
|
||||
*/
|
||||
messages: string | undefined;
|
||||
}) => VNode[];
|
||||
/**
|
||||
* Custom content when there is no selected file.
|
||||
*/
|
||||
empty: () => VNode[];
|
||||
}
|
||||
|
@ -252,6 +317,11 @@ export declare type FileUploadEmits = {
|
|||
* @param {FileUploadRemoveEvent} event - Custom remove event.
|
||||
*/
|
||||
remove: (event: FileUploadRemoveEvent) => void;
|
||||
/**
|
||||
* Callback to invoke when a single uploaded file is removed from the uploaded file list.
|
||||
* @param {FileUploadRemoveUploadedFile} event - Custom uploaded file remove event.
|
||||
*/
|
||||
removeUploadedFile: (event: FileUploadRemoveUploadedFile) => void;
|
||||
};
|
||||
|
||||
declare class FileUpload extends ClassComponent<FileUploadProps, FileUploadSlots, FileUploadEmits> {}
|
||||
|
|
|
@ -1,30 +1,24 @@
|
|||
<template>
|
||||
<div v-if="isAdvanced" class="p-fileupload p-fileupload-advanced p-component">
|
||||
<input ref="fileInput" type="file" @change="onFileSelect" :multiple="multiple" :accept="accept" :disabled="chooseDisabled" />
|
||||
<div class="p-fileupload-buttonbar">
|
||||
<span v-ripple :class="advancedChooseButtonClass" :style="style" @click="choose" @keydown.enter="choose" @focus="onFocus" @blur="onBlur" tabindex="0">
|
||||
<input ref="fileInput" type="file" @change="onFileSelect" :multiple="multiple" :accept="accept" :disabled="chooseDisabled" />
|
||||
<span :class="advancedChooseIconClass"></span>
|
||||
<span class="p-button-label">{{ chooseButtonLabel }}</span>
|
||||
</span>
|
||||
<FileUploadButton v-if="showUploadButton" :label="uploadButtonLabel" :icon="uploadIcon" @click="upload" :disabled="uploadDisabled" />
|
||||
<FileUploadButton v-if="showCancelButton" :label="cancelButtonLabel" :icon="cancelIcon" @click="clear" :disabled="cancelDisabled" />
|
||||
<slot name="header" :files="files" :uploadedFiles="uploadedFiles" :chooseCallback="choose" :uploadCallback="upload" :clearCallback="clear">
|
||||
<span v-ripple :class="advancedChooseButtonClass" :style="style" @click="choose" @keydown.enter="choose" @focus="onFocus" @blur="onBlur" tabindex="0">
|
||||
<span :class="advancedChooseIconClass"></span>
|
||||
<span class="p-button-label">{{ chooseButtonLabel }}</span>
|
||||
</span>
|
||||
<FileUploadButton v-if="showUploadButton" :label="uploadButtonLabel" :icon="uploadIcon" @click="upload" :disabled="uploadDisabled" />
|
||||
<FileUploadButton v-if="showCancelButton" :label="cancelButtonLabel" :icon="cancelIcon" @click="clear" :disabled="cancelDisabled" />
|
||||
</slot>
|
||||
</div>
|
||||
<div ref="content" class="p-fileupload-content" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<FileUploadProgressBar v-if="hasFiles" :value="progress" />
|
||||
<FileUploadMessage v-for="msg of messages" :key="msg" severity="error" @close="onMessageClose">{{ msg }}</FileUploadMessage>
|
||||
<div v-if="hasFiles" class="p-fileupload-files">
|
||||
<div v-for="(file, index) of files" :key="file.name + file.type + file.size" class="p-fileupload-row">
|
||||
<div>
|
||||
<img v-if="isImage(file)" role="presentation" :alt="file.name" :src="file.objectURL" :width="previewWidth" />
|
||||
</div>
|
||||
<div class="p-fileupload-filename">{{ file.name }}</div>
|
||||
<div>{{ formatSize(file.size) }}</div>
|
||||
<div>
|
||||
<FileUploadButton type="button" icon="pi pi-times" @click="remove(index)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$slots.empty && !hasFiles" class="p-fileupload-empty">
|
||||
<slot name="content" :files="files" :uploadedFiles="uploadedFiles" :removeUploadedFileCallback="removeUploadedFile" :removeFileCallback="remove" :progress="progress" :messages="messages">
|
||||
<FileUploadProgressBar v-if="hasFiles" :value="progress" :showValue="false" />
|
||||
<FileUploadMessage v-for="msg of messages" :key="msg" severity="error" @close="onMessageClose">{{ msg }}</FileUploadMessage>
|
||||
<FileContent v-if="hasFiles" :files="files" @remove="remove" :badgeValue="pendingLabel" :previewWidth="previewWidth" />
|
||||
<FileContent :files="uploadedFiles" @remove="removeUploadedFile" :badgeValue="completedLabel" badgeSeverity="success" :previewWidth="previewWidth" />
|
||||
</slot>
|
||||
<div v-if="$slots.empty && !hasFiles && !hasUploadedFiles" class="p-fileupload-empty">
|
||||
<slot name="empty"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,14 +35,15 @@
|
|||
|
||||
<script>
|
||||
import Button from 'primevue/button';
|
||||
import ProgressBar from 'primevue/progressbar';
|
||||
import Message from 'primevue/message';
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
import ProgressBar from 'primevue/progressbar';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
import FileContent from './FileContent.vue';
|
||||
|
||||
export default {
|
||||
name: 'FileUpload',
|
||||
emits: ['select', 'uploader', 'before-upload', 'progress', 'upload', 'error', 'before-send', 'clear', 'remove'],
|
||||
emits: ['select', 'uploader', 'before-upload', 'progress', 'upload', 'error', 'before-send', 'clear', 'remove', 'remove-uploaded-file'],
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
|
@ -152,7 +147,8 @@ export default {
|
|||
files: [],
|
||||
messages: [],
|
||||
focused: false,
|
||||
progress: null
|
||||
progress: null,
|
||||
uploadedFiles: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -250,6 +246,7 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
this.uploadedFiles.push(...this.files);
|
||||
this.clear();
|
||||
}
|
||||
};
|
||||
|
@ -379,6 +376,15 @@ export default {
|
|||
files: this.files
|
||||
});
|
||||
},
|
||||
removeUploadedFile(index) {
|
||||
let removedFile = this.uploadedFiles.splice(index, 1)[0];
|
||||
|
||||
this.uploadedFiles = [...this.uploadedFiles];
|
||||
this.$emit('remove-uploaded-file', {
|
||||
file: removedFile,
|
||||
files: this.uploadedFiles
|
||||
});
|
||||
},
|
||||
clearInputElement() {
|
||||
this.$refs.fileInput.value = '';
|
||||
},
|
||||
|
@ -456,6 +462,9 @@ export default {
|
|||
hasFiles() {
|
||||
return this.files && this.files.length > 0;
|
||||
},
|
||||
hasUploadedFiles() {
|
||||
return this.uploadedFiles && this.uploadedFiles.length > 0;
|
||||
},
|
||||
chooseDisabled() {
|
||||
return this.disabled || (this.fileLimit && this.fileLimit <= this.files.length + this.uploadedFileCount);
|
||||
},
|
||||
|
@ -473,12 +482,19 @@ export default {
|
|||
},
|
||||
cancelButtonLabel() {
|
||||
return this.cancelLabel || this.$primevue.config.locale.cancel;
|
||||
},
|
||||
completedLabel() {
|
||||
return this.$primevue.config.locale.completed;
|
||||
},
|
||||
pendingLabel() {
|
||||
return this.$primevue.config.locale.pending;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FileUploadButton: Button,
|
||||
FileUploadProgressBar: ProgressBar,
|
||||
FileUploadMessage: Message
|
||||
FileUploadMessage: Message,
|
||||
FileContent
|
||||
},
|
||||
directives: {
|
||||
ripple: Ripple
|
||||
|
@ -491,20 +507,6 @@ export default {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.p-fileupload-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-fileupload-row > div {
|
||||
flex: 1 1 auto;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.p-fileupload-row > div:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.p-fileupload-content .p-progressbar {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
|
@ -517,19 +519,31 @@ export default {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.p-button.p-fileupload-choose input[type='file'] {
|
||||
display: none;
|
||||
.p-fileupload-buttonbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.p-fileupload-choose.p-fileupload-choose-selected input[type='file'] {
|
||||
.p-fileupload > input[type='file'],
|
||||
.p-fileupload-basic input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-fileupload-filename {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.p-fluid .p-fileupload .p-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.p-fileupload-file {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-fileupload-file-thumbnail {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.p-fileupload-file-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { ObjectDirective } from 'vue';
|
||||
|
||||
declare const Ripple: ObjectDirective;
|
||||
|
||||
export default Ripple;
|
|
@ -0,0 +1,114 @@
|
|||
import { DomHandler, ObjectUtils } from 'primevue/utils';
|
||||
|
||||
function bind(el, binding) {
|
||||
const { onFocusIn, onFocusOut } = binding.value || {};
|
||||
|
||||
el.$_pfocustrap_mutationobserver = new MutationObserver((mutationList) => {
|
||||
mutationList.forEach((mutation) => {
|
||||
if (mutation.type === 'childList' && !el.contains(document.activeElement)) {
|
||||
const findNextFocusableElement = (el) => {
|
||||
const focusableElement = DomHandler.isFocusableElement(el) ? el : DomHandler.getFirstFocusableElement(el);
|
||||
|
||||
return ObjectUtils.isNotEmpty(focusableElement) ? focusableElement : findNextFocusableElement(el.nextSibling);
|
||||
};
|
||||
|
||||
DomHandler.focus(findNextFocusableElement(mutation.nextSibling));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
el.$_pfocustrap_mutationobserver.disconnect();
|
||||
el.$_pfocustrap_mutationobserver.observe(el, {
|
||||
childList: true
|
||||
});
|
||||
|
||||
el.$_pfocustrap_focusinlistener = (event) => onFocusIn && onFocusIn(event);
|
||||
el.$_pfocustrap_focusoutlistener = (event) => onFocusOut && onFocusOut(event);
|
||||
|
||||
el.addEventListener('focusin', el.$_pfocustrap_focusinlistener);
|
||||
el.addEventListener('focusout', el.$_pfocustrap_focusoutlistener);
|
||||
}
|
||||
|
||||
function unbind(el) {
|
||||
el.$_pfocustrap_mutationobserver && el.$_pfocustrap_mutationobserver.disconnect();
|
||||
el.$_pfocustrap_focusinlistener && el.removeEventListener('focusin', el.$_pfocustrap_focusinlistener) && (el.$_pfocustrap_focusinlistener = null);
|
||||
el.$_pfocustrap_focusoutlistener && el.removeEventListener('focusout', el.$_pfocustrap_focusoutlistener) && (el.$_pfocustrap_focusoutlistener = null);
|
||||
}
|
||||
|
||||
function autoFocus(el, binding) {
|
||||
const { autoFocusSelector = '', firstFocusableSelector = '', autoFocus = false } = binding.value || {};
|
||||
let focusableElement = DomHandler.getFirstFocusableElement(el, `[autofocus]:not(.p-hidden-focusable)${autoFocusSelector}`);
|
||||
|
||||
autoFocus && !focusableElement && (focusableElement = DomHandler.getFirstFocusableElement(el, `:not(.p-hidden-focusable)${firstFocusableSelector}`));
|
||||
DomHandler.focus(focusableElement);
|
||||
}
|
||||
|
||||
function onFirstHiddenElementFocus(event) {
|
||||
const { currentTarget, relatedTarget } = event;
|
||||
const focusableElement =
|
||||
relatedTarget === currentTarget.$_pfocustrap_lasthiddenfocusableelement
|
||||
? DomHandler.getFirstFocusableElement(currentTarget.parentElement, `:not(.p-hidden-focusable)${currentTarget.$_pfocustrap_focusableselector}`)
|
||||
: currentTarget.$_pfocustrap_lasthiddenfocusableelement;
|
||||
|
||||
DomHandler.focus(focusableElement);
|
||||
}
|
||||
|
||||
function onLastHiddenElementFocus(event) {
|
||||
const { currentTarget, relatedTarget } = event;
|
||||
const focusableElement =
|
||||
relatedTarget === currentTarget.$_pfocustrap_firsthiddenfocusableelement
|
||||
? DomHandler.getLastFocusableElement(currentTarget.parentElement, `:not(.p-hidden-focusable)${currentTarget.$_pfocustrap_focusableselector}`)
|
||||
: currentTarget.$_pfocustrap_firsthiddenfocusableelement;
|
||||
|
||||
DomHandler.focus(focusableElement);
|
||||
}
|
||||
|
||||
function createHiddenFocusableElements(el, binding) {
|
||||
const { tabIndex = 0, firstFocusableSelector = '', lastFocusableSelector = '' } = binding.value || {};
|
||||
|
||||
const createFocusableElement = (onFocus) => {
|
||||
const element = document.createElement('span');
|
||||
|
||||
element.classList = 'p-hidden-accessible p-hidden-focusable';
|
||||
element.tabIndex = tabIndex;
|
||||
element.setAttribute('aria-hidden', 'true');
|
||||
element.setAttribute('role', 'presentation');
|
||||
element.addEventListener('focus', onFocus);
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
const firstFocusableElement = createFocusableElement(onFirstHiddenElementFocus);
|
||||
const lastFocusableElement = createFocusableElement(onLastHiddenElementFocus);
|
||||
|
||||
firstFocusableElement.$_pfocustrap_lasthiddenfocusableelement = lastFocusableElement;
|
||||
firstFocusableElement.$_pfocustrap_focusableselector = firstFocusableSelector;
|
||||
|
||||
lastFocusableElement.$_pfocustrap_firsthiddenfocusableelement = firstFocusableElement;
|
||||
lastFocusableElement.$_pfocustrap_focusableselector = lastFocusableSelector;
|
||||
|
||||
el.prepend(firstFocusableElement);
|
||||
el.append(lastFocusableElement);
|
||||
}
|
||||
|
||||
const FocusTrap = {
|
||||
mounted(el, binding) {
|
||||
const { disabled } = binding.value || {};
|
||||
|
||||
if (!disabled) {
|
||||
createHiddenFocusableElements(el, binding);
|
||||
bind(el, binding);
|
||||
autoFocus(el, binding);
|
||||
}
|
||||
},
|
||||
updated(el, binding) {
|
||||
const { disabled } = binding.value || {};
|
||||
|
||||
disabled && unbind(el);
|
||||
},
|
||||
unmounted(el) {
|
||||
unbind(el);
|
||||
}
|
||||
};
|
||||
|
||||
export default FocusTrap;
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"main": "./focustrap.cjs.js",
|
||||
"module": "./focustrap.esm.js",
|
||||
"unpkg": "./focustrap.min.js",
|
||||
"types": "./FocusTrap.d.ts"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { VNode } from 'vue';
|
||||
import { ButtonHTMLAttributes, HTMLAttributes, VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
type GalleriaThumbnailsPositionType = 'bottom' | 'top' | 'left' | 'right' | undefined;
|
||||
|
@ -119,11 +119,23 @@ export interface GalleriaProps {
|
|||
/**
|
||||
* Inline style of the component on fullscreen mode. Otherwise, the 'style' property can be used.
|
||||
*/
|
||||
containerStyle?: any;
|
||||
containerStyle?: any | undefined;
|
||||
/**
|
||||
* Style class of the component on fullscreen mode. Otherwise, the 'class' property can be used.
|
||||
*/
|
||||
containerClass?: any;
|
||||
containerClass?: any | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLDivElement to the container element on fullscreen mode.
|
||||
*/
|
||||
containerProps?: HTMLAttributes | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLButtonElement to the previous navigation button.
|
||||
*/
|
||||
prevButtonProps?: ButtonHTMLAttributes | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLButtonElement to the next navigation button.
|
||||
*/
|
||||
nextButtonProps?: ButtonHTMLAttributes | undefined;
|
||||
}
|
||||
|
||||
export interface GalleriaSlots {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from '../config/PrimeVue';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Galleria from './Galleria.vue';
|
||||
|
||||
describe('Gallleria.vue', () => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<Portal v-if="fullScreen">
|
||||
<div v-if="containerVisible" :ref="maskRef" :class="maskContentClass">
|
||||
<div v-if="containerVisible" :ref="maskRef" :class="maskContentClass" :role="fullScreen ? 'dialog' : 'region'" :aria-modal="fullScreen ? 'true' : undefined">
|
||||
<transition name="p-galleria" @before-enter="onBeforeEnter" @enter="onEnter" @before-leave="onBeforeLeave" @after-leave="onAfterLeave" appear>
|
||||
<GalleriaContent v-if="visible" :ref="containerRef" v-bind="$props" @mask-hide="maskHide" :templates="$slots" @activeitem-change="onActiveItemChange" />
|
||||
<GalleriaContent v-if="visible" :ref="containerRef" v-focustrap v-bind="$props" @mask-hide="maskHide" :templates="$slots" @activeitem-change="onActiveItemChange" />
|
||||
</transition>
|
||||
</div>
|
||||
</Portal>
|
||||
|
@ -10,9 +10,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import GalleriaContent from './GalleriaContent.vue';
|
||||
import { DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import FocusTrap from 'primevue/focustrap';
|
||||
import Portal from 'primevue/portal';
|
||||
import { DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import GalleriaContent from './GalleriaContent.vue';
|
||||
|
||||
export default {
|
||||
name: 'Galleria',
|
||||
|
@ -107,8 +108,26 @@ export default {
|
|||
type: String,
|
||||
default: null
|
||||
},
|
||||
containerStyle: null,
|
||||
containerClass: null
|
||||
containerStyle: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
containerClass: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
containerProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
prevButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
nextButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
container: null,
|
||||
mask: null,
|
||||
|
@ -141,6 +160,7 @@ export default {
|
|||
onEnter(el) {
|
||||
this.mask.style.zIndex = String(parseInt(el.style.zIndex, 10) - 1);
|
||||
DomHandler.addClass(document.body, 'p-overflow-hidden');
|
||||
this.focus();
|
||||
},
|
||||
onBeforeLeave() {
|
||||
DomHandler.addClass(this.mask, 'p-component-overlay-leave');
|
||||
|
@ -163,6 +183,13 @@ export default {
|
|||
},
|
||||
maskRef(el) {
|
||||
this.mask = el;
|
||||
},
|
||||
focus() {
|
||||
let focusTarget = this.container.$el.querySelector('[autofocus]');
|
||||
|
||||
if (focusTarget) {
|
||||
focusTarget.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -180,6 +207,9 @@ export default {
|
|||
components: {
|
||||
GalleriaContent: GalleriaContent,
|
||||
Portal: Portal
|
||||
},
|
||||
directives: {
|
||||
focustrap: FocusTrap
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<template>
|
||||
<div v-if="$attrs.value && $attrs.value.length > 0" :id="id" :class="galleriaClass" :style="$attrs.containerStyle">
|
||||
<button v-if="$attrs.fullScreen" v-ripple type="button" class="p-galleria-close p-link" @click="$emit('mask-hide')">
|
||||
<div v-if="$attrs.value && $attrs.value.length > 0" :id="id" :class="galleriaClass" :style="$attrs.containerStyle" v-bind="$attrs.containerProps">
|
||||
<button v-if="$attrs.fullScreen" v-ripple autofocus type="button" class="p-galleria-close p-link" :aria-label="closeAriaLabel" @click="$emit('mask-hide')">
|
||||
<span class="p-galleria-close-icon pi pi-times"></span>
|
||||
</button>
|
||||
<div v-if="$attrs.templates && $attrs.templates['header']" class="p-galleria-header">
|
||||
<component :is="$attrs.templates['header']" />
|
||||
</div>
|
||||
<div class="p-galleria-content">
|
||||
<div class="p-galleria-content" :aria-live="$attrs.autoPlay ? 'polite' : 'off'">
|
||||
<GalleriaItem
|
||||
:id="id"
|
||||
v-model:activeIndex="activeIndex"
|
||||
v-model:slideShowActive="slideShowActive"
|
||||
:value="$attrs.value"
|
||||
|
@ -34,6 +35,8 @@
|
|||
:isVertical="isVertical()"
|
||||
:contentHeight="$attrs.verticalThumbnailViewPortHeight"
|
||||
:showThumbnailNavigators="$attrs.showThumbnailNavigators"
|
||||
:prevButtonProps="$attrs.prevButtonProps"
|
||||
:nextButtonProps="$attrs.nextButtonProps"
|
||||
@stop-slideshow="stopSlideShow"
|
||||
/>
|
||||
</div>
|
||||
|
@ -44,11 +47,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { UniqueComponentId } from 'primevue/utils';
|
||||
import GalleriaItem from './GalleriaItem.vue';
|
||||
import GalleriaThumbnails from './GalleriaThumbnails.vue';
|
||||
import GalleriaItemSlot from './GalleriaItemSlot.vue';
|
||||
import Ripple from 'primevue/ripple';
|
||||
|
||||
export default {
|
||||
name: 'GalleriaContent',
|
||||
|
@ -130,12 +132,14 @@ export default {
|
|||
indicatorPosClass,
|
||||
this.$attrs.containerClass
|
||||
];
|
||||
},
|
||||
closeAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.close : undefined;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
GalleriaItem: GalleriaItem,
|
||||
GalleriaThumbnails: GalleriaThumbnails,
|
||||
GalleriaItemSlot: GalleriaItemSlot
|
||||
GalleriaThumbnails: GalleriaThumbnails
|
||||
},
|
||||
directives: {
|
||||
ripple: Ripple
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<button v-if="showItemNavigators" v-ripple type="button" :class="navBackwardClass" @click="navBackward($event)" :disabled="isNavBackwardDisabled()">
|
||||
<span class="p-galleria-item-prev-icon pi pi-chevron-left"></span>
|
||||
</button>
|
||||
<div class="p-galleria-item">
|
||||
<div :id="id + '_item_' + activeIndex" class="p-galleria-item" role="group" :aria-label="ariaSlideNumber(activeIndex + 1)" :aria-roledescription="ariaSlideLabel">
|
||||
<component v-if="templates.item" :is="templates.item" :item="activeItem" />
|
||||
</div>
|
||||
<button v-if="showItemNavigators" v-ripple type="button" :class="navForwardClass" @click="navForward($event)" :disabled="isNavForwardDisabled()">
|
||||
|
@ -18,11 +18,14 @@
|
|||
<li
|
||||
v-for="(item, index) of value"
|
||||
:key="`p-galleria-indicator-${index}`"
|
||||
:class="['p-galleria-indicator', { 'p-highlight': isIndicatorItemActive(index) }]"
|
||||
tabindex="0"
|
||||
:aria-label="ariaPageLabel(index + 1)"
|
||||
:aria-selected="activeIndex === index"
|
||||
:aria-controls="id + '_item_' + index"
|
||||
@click="onIndicatorClick(index)"
|
||||
@mouseenter="onIndicatorMouseEnter(index)"
|
||||
@keydown.enter="onIndicatorKeyDown(index)"
|
||||
:class="['p-galleria-indicator', { 'p-highlight': isIndicatorItemActive(index) }]"
|
||||
@keydown="onIndicatorKeyDown($event, index)"
|
||||
>
|
||||
<button v-if="!templates['indicator']" type="button" tabindex="-1" class="p-link"></button>
|
||||
<component v-if="templates.indicator" :is="templates.indicator" :index="index" />
|
||||
|
@ -73,6 +76,10 @@ export default {
|
|||
templates: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -125,10 +132,24 @@ export default {
|
|||
this.$emit('update:activeIndex', index);
|
||||
}
|
||||
},
|
||||
onIndicatorKeyDown(index) {
|
||||
this.stopSlideShow();
|
||||
onIndicatorKeyDown(event, index) {
|
||||
switch (event.code) {
|
||||
case 'Enter':
|
||||
case 'Space':
|
||||
this.stopSlideShow();
|
||||
|
||||
this.$emit('update:activeIndex', index);
|
||||
this.$emit('update:activeIndex', index);
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
isIndicatorItemActive(index) {
|
||||
return this.activeIndex === index;
|
||||
|
@ -138,6 +159,12 @@ export default {
|
|||
},
|
||||
isNavForwardDisabled() {
|
||||
return !this.circular && this.activeIndex === this.value.length - 1;
|
||||
},
|
||||
ariaSlideNumber(value) {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.slideNumber.replace(/{slideNumber}/g, value) : undefined;
|
||||
},
|
||||
ariaPageLabel(value) {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.pageLabel.replace(/{page}/g, value) : undefined;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -159,6 +186,9 @@ export default {
|
|||
'p-disabled': this.isNavForwardDisabled()
|
||||
}
|
||||
];
|
||||
},
|
||||
ariaSlideLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.slide : undefined;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
functional: true,
|
||||
props: {
|
||||
item: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
templates: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
render(createElement, context) {
|
||||
const { item, index, templates, type } = context.props;
|
||||
const template = templates && templates[type];
|
||||
|
||||
if (template) {
|
||||
let content;
|
||||
|
||||
switch (type) {
|
||||
case 'item':
|
||||
case 'caption':
|
||||
case 'thumbnail':
|
||||
content = template({
|
||||
item
|
||||
});
|
||||
break;
|
||||
case 'indicator':
|
||||
content = template({
|
||||
index
|
||||
});
|
||||
break;
|
||||
default:
|
||||
content = template({});
|
||||
break;
|
||||
}
|
||||
|
||||
return content ? [content] : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="p-galleria-thumbnail-wrapper">
|
||||
<div class="p-galleria-thumbnail-container">
|
||||
<button v-if="showThumbnailNavigators" v-ripple :class="navBackwardClass" @click="navBackward($event)" :disabled="isNavBackwardDisabled()" type="button">
|
||||
<button v-if="showThumbnailNavigators" v-ripple :class="navBackwardClass" :disabled="isNavBackwardDisabled()" type="button" :aria-label="ariaPrevButtonLabel" @click="navBackward($event)" v-bind="prevButtonProps">
|
||||
<span :class="navBackwardIconClass"></span>
|
||||
</button>
|
||||
<div class="p-galleria-thumbnail-items-container" :style="{ height: isVertical ? contentHeight : '' }">
|
||||
<div ref="itemsContainer" class="p-galleria-thumbnail-items" @transitionend="onTransitionEnd" @touchstart="onTouchStart($event)" @touchmove="onTouchMove($event)" @touchend="onTouchEnd($event)">
|
||||
<div ref="itemsContainer" class="p-galleria-thumbnail-items" role="tablist" @transitionend="onTransitionEnd" @touchstart="onTouchStart($event)" @touchmove="onTouchMove($event)" @touchend="onTouchEnd($event)">
|
||||
<div
|
||||
v-for="(item, index) of value"
|
||||
:key="`p-galleria-thumbnail-item-${index}`"
|
||||
|
@ -18,14 +18,18 @@
|
|||
'p-galleria-thumbnail-item-end': lastItemActiveIndex() === index
|
||||
}
|
||||
]"
|
||||
role="tab"
|
||||
:aria-selected="activeIndex === index"
|
||||
:aria-controls="containerId + '_item_' + index"
|
||||
@keydown="onThumbnailKeydown($event, index)"
|
||||
>
|
||||
<div class="p-galleria-thumbnail-item-content" :tabindex="isItemActive(index) ? 0 : null" @click="onItemClick(index)" @keydown.enter="onItemClick(index)">
|
||||
<div class="p-galleria-thumbnail-item-content" :tabindex="activeIndex === index ? '0' : '-1'" :aria-label="ariaPageLabel(index + 1)" :aria-current="activeIndex === index ? 'page' : undefined" @click="onItemClick(index)">
|
||||
<component v-if="templates.thumbnail" :is="templates.thumbnail" :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="showThumbnailNavigators" v-ripple :class="navForwardClass" @click="navForward($event)" :disabled="isNavForwardDisabled()" type="button">
|
||||
<button v-if="showThumbnailNavigators" v-ripple :class="navForwardClass" :disabled="isNavForwardDisabled()" type="button" :aria-label="ariaNextButtonLabel" @click="navForward($event)" v-bind="nextButtonProps">
|
||||
<span :class="navForwardIconClass"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -33,8 +37,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'GalleriaThumbnails',
|
||||
|
@ -83,6 +87,14 @@ export default {
|
|||
templates: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
prevButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
nextButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
startPos: null,
|
||||
|
@ -211,7 +223,7 @@ export default {
|
|||
navForward(e) {
|
||||
this.stopSlideShow();
|
||||
|
||||
let nextItemIndex = this.d_activeIndex + 1;
|
||||
let nextItemIndex = this.d_activeIndex === this.value.length - 1 ? this.value.length - 1 : this.d_activeIndex + 1;
|
||||
|
||||
if (nextItemIndex + this.totalShiftedItems > this.getMedianItemIndex() && (-1 * this.totalShiftedItems < this.getTotalPageNumber() - 1 || this.circular)) {
|
||||
this.step(-1);
|
||||
|
@ -251,6 +263,89 @@ export default {
|
|||
this.$emit('update:activeIndex', selectedItemIndex);
|
||||
}
|
||||
},
|
||||
onThumbnailKeydown(event, index) {
|
||||
if (event.code === 'Enter' || event.code === 'Space') {
|
||||
this.onItemClick(index);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
switch (event.code) {
|
||||
case 'ArrowRight':
|
||||
this.onRightKey();
|
||||
break;
|
||||
|
||||
case 'ArrowLeft':
|
||||
this.onLeftKey();
|
||||
break;
|
||||
|
||||
case 'Home':
|
||||
this.onHomeKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
this.onEndKey();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
this.onTabKey();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onRightKey() {
|
||||
const indicators = DomHandler.find(this.$refs.itemsContainer, '.p-galleria-thumbnail-item');
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, activeIndex + 1 === indicators.length ? indicators.length - 1 : activeIndex + 1);
|
||||
},
|
||||
onLeftKey() {
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, activeIndex - 1 <= 0 ? 0 : activeIndex - 1);
|
||||
},
|
||||
onHomeKey() {
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, 0);
|
||||
},
|
||||
onEndKey() {
|
||||
const indicators = DomHandler.find(this.$refs.itemsContainer, '.p-galleria-thumbnail-item');
|
||||
const activeIndex = this.findFocusedIndicatorIndex();
|
||||
|
||||
this.changedFocusedIndicator(activeIndex, indicators.length - 1);
|
||||
},
|
||||
onTabKey() {
|
||||
const indicators = [...DomHandler.find(this.$refs.itemsContainer, '.p-galleria-thumbnail-item')];
|
||||
const highlightedIndex = indicators.findIndex((ind) => DomHandler.hasClass(ind, 'p-galleria-thumbnail-item-current'));
|
||||
|
||||
const activeIndicator = DomHandler.findSingle(this.$refs.itemsContainer, '.p-galleria-thumbnail-item > [tabindex="0"');
|
||||
const activeIndex = indicators.findIndex((ind) => ind === activeIndicator.parentElement);
|
||||
|
||||
indicators[activeIndex].children[0].tabIndex = '-1';
|
||||
indicators[highlightedIndex].children[0].tabIndex = '0';
|
||||
},
|
||||
findFocusedIndicatorIndex() {
|
||||
const indicators = [...DomHandler.find(this.$refs.itemsContainer, '.p-galleria-thumbnail-item')];
|
||||
const activeIndicator = DomHandler.findSingle(this.$refs.itemsContainer, '.p-galleria-thumbnail-item > [tabindex="0"]');
|
||||
|
||||
return indicators.findIndex((ind) => ind === activeIndicator.parentElement);
|
||||
},
|
||||
changedFocusedIndicator(prevInd, nextInd) {
|
||||
const indicators = DomHandler.find(this.$refs.itemsContainer, '.p-galleria-thumbnail-item');
|
||||
|
||||
indicators[prevInd].children[0].tabIndex = '-1';
|
||||
indicators[nextInd].children[0].tabIndex = '0';
|
||||
indicators[nextInd].children[0].focus();
|
||||
},
|
||||
onTransitionEnd() {
|
||||
if (this.$refs.itemsContainer) {
|
||||
DomHandler.addClass(this.$refs.itemsContainer, 'p-items-hidden');
|
||||
|
@ -384,6 +479,9 @@ export default {
|
|||
},
|
||||
isItemActive(index) {
|
||||
return this.firstItemAciveIndex() <= index && this.lastItemActiveIndex() >= index;
|
||||
},
|
||||
ariaPageLabel(value) {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.pageLabel.replace(/{page}/g, value) : undefined;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -420,6 +518,12 @@ export default {
|
|||
'pi-chevron-down': this.isVertical
|
||||
}
|
||||
];
|
||||
},
|
||||
ariaPrevButtonLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.prevPageLabel : undefined;
|
||||
},
|
||||
ariaNextButtonLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.nextPageLabel : undefined;
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Image from './Image.vue';
|
||||
|
||||
describe('Image.vue', () => {
|
||||
it('should exist', () => {
|
||||
const wrapper = mount(Image, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
|
@ -21,6 +23,7 @@ describe('Image.vue', () => {
|
|||
it('should preview', async () => {
|
||||
const wrapper = mount(Image, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
teleport: true
|
||||
}
|
||||
|
@ -42,6 +45,8 @@ describe('Image.vue', () => {
|
|||
|
||||
await wrapper.setData({ maskVisible: false });
|
||||
|
||||
expect(wrapper.find('.p-image-mask').exists()).toBe(false);
|
||||
setTimeout(() => {
|
||||
expect(wrapper.find('.p-image-mask').exists()).toBe(false);
|
||||
}, 25);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
<template>
|
||||
<span :class="containerClass" :style="style">
|
||||
<img v-bind="$attrs" :style="imageStyle" :class="imageClass" @error="onError" />
|
||||
<div v-if="preview" class="p-image-preview-indicator" @click="onImageClick">
|
||||
<button v-if="preview" ref="previewButton" class="p-image-preview-indicator" @click="onImageClick" v-bind="previewButtonProps">
|
||||
<slot name="indicator">
|
||||
<i class="p-image-preview-icon pi pi-eye"></i>
|
||||
</slot>
|
||||
</div>
|
||||
</button>
|
||||
<Portal>
|
||||
<div v-if="maskVisible" :ref="maskRef" :class="maskClass" @click="onMaskClick">
|
||||
<div v-if="maskVisible" :ref="maskRef" v-focustrap role="dialog" :class="maskClass" :aria-modal="maskVisible" @click="onMaskClick" @keydown="onMaskKeydown">
|
||||
<div class="p-image-toolbar">
|
||||
<button class="p-image-action p-link" @click="rotateRight" type="button">
|
||||
<button class="p-image-action p-link" @click="rotateRight" type="button" :aria-label="rightAriaLabel">
|
||||
<i class="pi pi-refresh"></i>
|
||||
</button>
|
||||
<button class="p-image-action p-link" @click="rotateLeft" type="button">
|
||||
<button class="p-image-action p-link" @click="rotateLeft" type="button" :aria-label="leftAriaLabel">
|
||||
<i class="pi pi-undo"></i>
|
||||
</button>
|
||||
<button class="p-image-action p-link" @click="zoomOut" type="button" :disabled="zoomDisabled">
|
||||
<button class="p-image-action p-link" @click="zoomOut" type="button" :disabled="zoomDisabled" :aria-label="zoomOutAriaLabel">
|
||||
<i class="pi pi-search-minus"></i>
|
||||
</button>
|
||||
<button class="p-image-action p-link" @click="zoomIn" type="button" :disabled="zoomDisabled">
|
||||
<button class="p-image-action p-link" @click="zoomIn" type="button" :disabled="zoomDisabled" :aria-label="zoomInAriaLabel">
|
||||
<i class="pi pi-search-plus"></i>
|
||||
</button>
|
||||
<button class="p-image-action p-link" type="button" @click="hidePreview">
|
||||
<button class="p-image-action p-link" type="button" @click="hidePreview" :aria-label="closeAriaLabel" autofocus>
|
||||
<i class="pi pi-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -36,8 +36,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
import FocusTrap from 'primevue/focustrap';
|
||||
import Portal from 'primevue/portal';
|
||||
import { DomHandler, ZIndexUtils } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'Image',
|
||||
|
@ -48,10 +49,26 @@ export default {
|
|||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
class: null,
|
||||
style: null,
|
||||
imageStyle: null,
|
||||
imageClass: null
|
||||
class: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
style: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
imageStyle: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
imageClass: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
previewButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
mask: null,
|
||||
data() {
|
||||
|
@ -94,6 +111,21 @@ export default {
|
|||
|
||||
this.previewClick = false;
|
||||
},
|
||||
onMaskKeydown(event) {
|
||||
switch (event.code) {
|
||||
case 'Escape':
|
||||
this.onMaskClick();
|
||||
setTimeout(() => {
|
||||
DomHandler.focus(this.$refs.previewButton);
|
||||
}, 25);
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onError() {
|
||||
this.$emit('error');
|
||||
},
|
||||
|
@ -117,6 +149,7 @@ export default {
|
|||
ZIndexUtils.set('modal', this.mask, this.$primevue.config.zIndex.modal);
|
||||
},
|
||||
onEnter() {
|
||||
this.focus();
|
||||
this.$emit('show');
|
||||
},
|
||||
onBeforeLeave() {
|
||||
|
@ -128,6 +161,13 @@ export default {
|
|||
onAfterLeave(el) {
|
||||
ZIndexUtils.clear(el);
|
||||
this.maskVisible = false;
|
||||
},
|
||||
focus() {
|
||||
let focusTarget = this.mask.querySelector('[autofocus]');
|
||||
|
||||
if (focusTarget) {
|
||||
focusTarget.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -151,10 +191,28 @@ export default {
|
|||
},
|
||||
zoomDisabled() {
|
||||
return this.scale <= 0.5 || this.scale >= 1.5;
|
||||
},
|
||||
rightAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.rotateRight : undefined;
|
||||
},
|
||||
leftAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.rotateLeft : undefined;
|
||||
},
|
||||
zoomInAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.zoomIn : undefined;
|
||||
},
|
||||
zoomOutAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.zoomOut : undefined;
|
||||
},
|
||||
closeAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.close : undefined;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Portal: Portal
|
||||
},
|
||||
directives: {
|
||||
focustrap: FocusTrap
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VNode } from 'vue';
|
||||
import { HTMLAttributes, ButtonHTMLAttributes, VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
export interface InplaceProps {
|
||||
|
@ -14,6 +14,19 @@ export interface InplaceProps {
|
|||
* When present, it specifies that the element should be disabled.
|
||||
*/
|
||||
disabled?: boolean | undefined;
|
||||
/**
|
||||
* Icon to display in the close button.
|
||||
* Default value is 'pi pi-times'.
|
||||
*/
|
||||
closeIcon?: string | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLDivElement to display container.
|
||||
*/
|
||||
displayProps?: HTMLAttributes | undefined;
|
||||
/**
|
||||
* Uses to pass all properties of the HTMLButtonElement to the close button.
|
||||
*/
|
||||
closeButtonProps?: ButtonHTMLAttributes | undefined;
|
||||
}
|
||||
|
||||
export interface InplaceSlots {
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { config, mount } from '@vue/test-utils';
|
||||
import Inplace from './Inplace.vue';
|
||||
import InputText from '../inputtext/InputText.vue';
|
||||
import InputText from '@/components/inputtext/InputText.vue';
|
||||
|
||||
config.global.mocks = {
|
||||
$primevue: {
|
||||
config: {
|
||||
locale: {
|
||||
aria: {
|
||||
close: 'Close'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('Inplace.vue', () => {
|
||||
it('should exist', () => {
|
||||
|
@ -64,4 +76,26 @@ describe('Inplace.vue', () => {
|
|||
|
||||
expect(wrapper.find('.pi.pi-times').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should have custom close icon', async () => {
|
||||
const wrapper = mount(Inplace, {
|
||||
global: {
|
||||
components: {
|
||||
InputText
|
||||
}
|
||||
},
|
||||
props: {
|
||||
closable: true,
|
||||
closeIcon: 'pi pi-discord'
|
||||
},
|
||||
slots: {
|
||||
display: `{{'Click to Edit'}}`,
|
||||
content: `<InputText autoFocus />`
|
||||
}
|
||||
});
|
||||
|
||||
await wrapper.vm.open({});
|
||||
|
||||
expect(wrapper.find('.pi.pi-discord').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div :class="containerClass">
|
||||
<div v-if="!d_active" :class="displayClass" :tabindex="$attrs.tabindex || '0'" @click="open" @keydown.enter="open">
|
||||
<div :class="containerClass" aria-live="polite">
|
||||
<div v-if="!d_active" ref="display" :class="displayClass" :tabindex="$attrs.tabindex || '0'" role="button" @click="open" @keydown.enter="open" v-bind="displayProps">
|
||||
<slot name="display"></slot>
|
||||
</div>
|
||||
<div v-else class="p-inplace-content">
|
||||
<slot name="content"></slot>
|
||||
<IPButton v-if="closable" icon="pi pi-times" @click="close"></IPButton>
|
||||
<IPButton v-if="closable" :icon="closeIcon" :aria-label="closeAriaLabel" @click="close" v-bind="closeButtonProps" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -28,6 +28,18 @@ export default {
|
|||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closeIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-times'
|
||||
},
|
||||
displayProps: {
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
closeButtonProps: {
|
||||
type: null,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -54,6 +66,9 @@ export default {
|
|||
this.$emit('close', event);
|
||||
this.d_active = false;
|
||||
this.$emit('update:active', false);
|
||||
setTimeout(() => {
|
||||
this.$refs.display.focus();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -62,6 +77,9 @@ export default {
|
|||
},
|
||||
displayClass() {
|
||||
return ['p-inplace-display', { 'p-disabled': this.disabled }];
|
||||
},
|
||||
closeAriaLabel() {
|
||||
return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.close : undefined;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -134,6 +134,11 @@ export interface InputNumberProps {
|
|||
* Default value is true.
|
||||
*/
|
||||
allowEmpty?: boolean | undefined;
|
||||
/**
|
||||
* Highlights automatically the input value.
|
||||
* Default value is false.
|
||||
*/
|
||||
highlightOnFocus?: boolean | undefined;
|
||||
/**
|
||||
* When present, it specifies that the component should be disabled.
|
||||
*/
|
||||
|
|
|
@ -18,24 +18,24 @@ describe('InputNumber.vue', () => {
|
|||
});
|
||||
|
||||
it('is keydown called when down and up keys pressed', async () => {
|
||||
await wrapper.vm.onInputKeyDown({ which: 38, target: { value: 1 }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'ArrowUp', target: { value: 1 }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][0]).toEqual([2]);
|
||||
|
||||
await wrapper.vm.onInputKeyDown({ which: 40, target: { value: 2 }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'ArrowDown', target: { value: 2 }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][1]).toEqual([1]);
|
||||
});
|
||||
|
||||
it('is keydown called when tab key pressed', async () => {
|
||||
await wrapper.vm.onInputKeyDown({ which: 9, target: { value: '12' }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'Tab', target: { value: '12' }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][0]).toEqual([12]);
|
||||
expect(wrapper.find('input.p-inputnumber-input').attributes()['aria-valuenow']).toBe('12');
|
||||
});
|
||||
|
||||
it('is keydown called when enter key pressed', async () => {
|
||||
await wrapper.vm.onInputKeyDown({ which: 13, target: { value: '12' }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'Enter', target: { value: '12' }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][0]).toEqual([12]);
|
||||
expect(wrapper.find('input.p-inputnumber-input').attributes()['aria-valuenow']).toBe('12');
|
||||
|
@ -60,11 +60,11 @@ describe('InputNumber.vue', () => {
|
|||
it('should have min boundary', async () => {
|
||||
await wrapper.setProps({ modelValue: 95, min: 95 });
|
||||
|
||||
await wrapper.vm.onInputKeyDown({ which: 40, target: { value: 96 }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'ArrowDown', target: { value: 96 }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][0]).toEqual([95]);
|
||||
|
||||
await wrapper.vm.onInputKeyDown({ which: 40, target: { value: 95 }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'ArrowDown', target: { value: 95 }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][1]).toEqual([95]);
|
||||
});
|
||||
|
@ -72,11 +72,11 @@ describe('InputNumber.vue', () => {
|
|||
it('should have max boundary', async () => {
|
||||
await wrapper.setProps({ modelValue: 99, max: 100 });
|
||||
|
||||
await wrapper.vm.onInputKeyDown({ which: 38, target: { value: 99 }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'ArrowUp', target: { value: 99 }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][0]).toEqual([100]);
|
||||
|
||||
await wrapper.vm.onInputKeyDown({ which: 38, target: { value: 100 }, preventDefault: () => {} });
|
||||
await wrapper.vm.onInputKeyDown({ code: 'ArrowUp', target: { value: 100 }, preventDefault: () => {} });
|
||||
|
||||
expect(wrapper.emitted()['update:modelValue'][1]).toEqual([100]);
|
||||
});
|
||||
|
|
|
@ -35,8 +35,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import { DomHandler } from 'primevue/utils';
|
||||
|
||||
export default {
|
||||
name: 'InputNumber',
|
||||
|
@ -130,6 +131,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
highlightOnFocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
@ -475,46 +480,40 @@ export default {
|
|||
event.preventDefault();
|
||||
}
|
||||
|
||||
switch (event.which) {
|
||||
//up
|
||||
case 38:
|
||||
switch (event.code) {
|
||||
case 'ArrowUp':
|
||||
this.spin(event, 1);
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//down
|
||||
case 40:
|
||||
case 'ArrowDown':
|
||||
this.spin(event, -1);
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//left
|
||||
case 37:
|
||||
case 'ArrowLeft':
|
||||
if (!this.isNumeralChar(inputValue.charAt(selectionStart - 1))) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
//right
|
||||
case 39:
|
||||
case 'ArrowRight':
|
||||
if (!this.isNumeralChar(inputValue.charAt(selectionStart))) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
//tab and enter
|
||||
case 9:
|
||||
case 13:
|
||||
case 'Tab':
|
||||
case 'Enter':
|
||||
newValueStr = this.validateValue(this.parseValue(inputValue));
|
||||
this.$refs.input.$el.value = this.formatValue(newValueStr);
|
||||
this.$refs.input.$el.setAttribute('aria-valuenow', newValueStr);
|
||||
this.updateModel(event, newValueStr);
|
||||
break;
|
||||
|
||||
//backspace
|
||||
case 8: {
|
||||
case 'Backspace': {
|
||||
event.preventDefault();
|
||||
|
||||
if (selectionStart === selectionEnd) {
|
||||
|
@ -556,8 +555,7 @@ export default {
|
|||
break;
|
||||
}
|
||||
|
||||
// del
|
||||
case 46:
|
||||
case 'Delete':
|
||||
event.preventDefault();
|
||||
|
||||
if (selectionStart === selectionEnd) {
|
||||
|
@ -598,8 +596,7 @@ export default {
|
|||
|
||||
break;
|
||||
|
||||
//home
|
||||
case 36:
|
||||
case 'Home':
|
||||
if (this.min) {
|
||||
this.updateModel(event, this.min);
|
||||
event.preventDefault();
|
||||
|
@ -607,8 +604,7 @@ export default {
|
|||
|
||||
break;
|
||||
|
||||
//end
|
||||
case 35:
|
||||
case 'End':
|
||||
if (this.max) {
|
||||
this.updateModel(event, this.max);
|
||||
event.preventDefault();
|
||||
|
@ -836,7 +832,9 @@ export default {
|
|||
return index || 0;
|
||||
},
|
||||
onInputClick() {
|
||||
if (!this.readonly) {
|
||||
const currentValue = this.$refs.input.$el.value;
|
||||
|
||||
if (!this.readonly && currentValue !== DomHandler.getSelection()) {
|
||||
this.initCursor();
|
||||
}
|
||||
},
|
||||
|
@ -869,7 +867,7 @@ export default {
|
|||
},
|
||||
handleOnInput(event, currentValue, newValue) {
|
||||
if (this.isValueChanged(currentValue, newValue)) {
|
||||
this.$emit('input', { originalEvent: event, value: newValue });
|
||||
this.$emit('input', { originalEvent: event, value: newValue, formattedValue: currentValue });
|
||||
}
|
||||
},
|
||||
isValueChanged(currentValue, newValue) {
|
||||
|
@ -978,7 +976,11 @@ export default {
|
|||
|
||||
this._decimal.lastIndex = 0;
|
||||
|
||||
return decimalCharIndex !== -1 ? val1.split(this._decimal)[0] + val2.slice(decimalCharIndex) : val1;
|
||||
if (this.suffixChar) {
|
||||
return val1.replace(this.suffixChar, '').split(this._decimal)[0] + val2.replace(this.suffixChar, '').slice(decimalCharIndex) + this.suffixChar;
|
||||
} else {
|
||||
return decimalCharIndex !== -1 ? val1.split(this._decimal)[0] + val2.slice(decimalCharIndex) : val1;
|
||||
}
|
||||
}
|
||||
|
||||
return val1;
|
||||
|
@ -1000,6 +1002,11 @@ export default {
|
|||
},
|
||||
onInputFocus(event) {
|
||||
this.focused = true;
|
||||
|
||||
if (!this.disabled && !this.readonly && this.$refs.input.$el.value !== DomHandler.getSelection() && this.highlightOnFocus) {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
onInputBlur(event) {
|
||||
|
|
|
@ -68,6 +68,22 @@
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.p-float-label .p-placeholder,
|
||||
.p-float-label input::placeholder,
|
||||
.p-float-label .p-inputtext::placeholder {
|
||||
opacity: 0;
|
||||
transition-property: all;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.p-float-label .p-focus .p-placeholder,
|
||||
.p-float-label input:focus::placeholder,
|
||||
.p-float-label .p-inputtext:focus::placeholder {
|
||||
opacity: 1;
|
||||
transition-property: all;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.p-input-icon-left,
|
||||
.p-input-icon-right {
|
||||
position: relative;
|
||||
|
|
|
@ -155,6 +155,11 @@ export interface ListboxProps {
|
|||
* Index of the element in tabbing order.
|
||||
*/
|
||||
tabindex?: number | string | undefined;
|
||||
/**
|
||||
* Icon to display in filter input.
|
||||
* Default value is 'pi pi-search'.
|
||||
*/
|
||||
filterIcon?: string | undefined;
|
||||
/**
|
||||
* Defines a string value that labels an interactive element.
|
||||
*/
|
||||
|
|
|
@ -14,6 +14,7 @@ config.global.mocks = {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('Listbox.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
|
@ -48,4 +49,17 @@ describe('Listbox.vue', () => {
|
|||
|
||||
expect(wrapper.findAll('li.p-listbox-item')[0].classes()).toContain('p-highlight');
|
||||
});
|
||||
|
||||
describe('filter', () => {
|
||||
it('should have correct custom icon', async () => {
|
||||
await wrapper.setProps({
|
||||
filter: true,
|
||||
filterIcon: 'pi pi-discord'
|
||||
});
|
||||
|
||||
const icon = wrapper.find('.p-listbox-filter-icon');
|
||||
|
||||
expect(icon.classes()).toContain('pi-discord');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
@keydown="onFilterKeyDown"
|
||||
v-bind="filterInputProps"
|
||||
/>
|
||||
<span class="p-listbox-filter-icon pi pi-search"></span>
|
||||
<span :class="['p-listbox-filter-icon', filterIcon]" />
|
||||
</div>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ filterResultMessageText }}
|
||||
|
@ -75,12 +75,6 @@
|
|||
<slot name="empty">{{ emptyMessageText }}</slot>
|
||||
</li>
|
||||
</ul>
|
||||
<span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ emptyMessageText }}
|
||||
</span>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="$slots.loader" v-slot:loader="{ options }">
|
||||
<slot name="loader" :options="options"></slot>
|
||||
|
@ -88,14 +82,20 @@
|
|||
</VirtualScroller>
|
||||
</div>
|
||||
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
|
||||
<span v-if="!options || (options && options.length === 0)" role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ emptyMessageText }}
|
||||
</span>
|
||||
<span role="status" aria-live="polite" class="p-hidden-accessible">
|
||||
{{ selectedMessageText }}
|
||||
</span>
|
||||
<span ref="lastHiddenFocusableElement" role="presentation" aria-hidden="true" class="p-hidden-accessible p-hidden-focusable" :tabindex="!disabled ? tabindex : -1" @focus="onLastHiddenFocus"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import { FilterService } from 'primevue/api';
|
||||
import Ripple from 'primevue/ripple';
|
||||
import { DomHandler, ObjectUtils, UniqueComponentId } from 'primevue/utils';
|
||||
import VirtualScroller from 'primevue/virtualscroller';
|
||||
|
||||
export default {
|
||||
|
@ -158,6 +158,10 @@ export default {
|
|||
type: String,
|
||||
default: null
|
||||
},
|
||||
filterIcon: {
|
||||
type: String,
|
||||
default: 'pi pi-search'
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { VNode } from 'vue';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
import { MenuItem } from '../menuitem';
|
||||
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
|
||||
|
||||
type MegaMenuOrientationType = 'horizontal' | 'vertical' | undefined;
|
||||
|
||||
|
@ -20,6 +20,22 @@ export interface MegaMenuProps {
|
|||
* Default value is true.
|
||||
*/
|
||||
exact?: boolean | undefined;
|
||||
/**
|
||||
* When present, it specifies that the component should be disabled.
|
||||
*/
|
||||
disabled?: boolean | undefined;
|
||||
/**
|
||||
* Index of the element in tabbing order.
|
||||
*/
|
||||
tabindex?: number | string | undefined;
|
||||
/**
|
||||
* Defines a string value that labels an interactive element.
|
||||
*/
|
||||
'aria-label'?: string | undefined;
|
||||
/**
|
||||
* Identifier of the underlying menu element.
|
||||
*/
|
||||
'aria-labelledby'?: string | undefined;
|
||||
}
|
||||
|
||||
export interface MegaMenuSlots {
|
||||
|
@ -43,7 +59,18 @@ export interface MegaMenuSlots {
|
|||
}) => VNode[];
|
||||
}
|
||||
|
||||
export declare type MegaMenuEmits = {};
|
||||
export declare type MegaMenuEmits = {
|
||||
/**
|
||||
* Callback to invoke when the component receives focus.
|
||||
* @param {Event} event - Browser event.
|
||||
*/
|
||||
focus: (event: Event) => void;
|
||||
/**
|
||||
* Callback to invoke when the component loses focus.
|
||||
* @param {Event} event - Browser event.
|
||||
*/
|
||||
blur: (event: Event) => void;
|
||||
};
|
||||
|
||||
declare class MegaMenu extends ClassComponent<MegaMenuProps, MegaMenuSlots, MegaMenuEmits> {}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue