Skip to content

Commit

Permalink
Merge branch 'main' into 362-feature-accessibility-no-possibility-to-…
Browse files Browse the repository at this point in the history
…uncheckcheckexpandable-checkbox-radio-expandable-components
  • Loading branch information
mattgoud authored Sep 17, 2024
2 parents ad3732c + 7016660 commit 84c05c5
Show file tree
Hide file tree
Showing 19 changed files with 1,211 additions and 4 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ $ pnpm cz
## Mono repository structure

This mono repository contains multiple packages under the folder `packages`

- `components` contains all the vue components and is released under the name `@prestashopcorp/puik-components`
- `locale` contains all the translations files for the default wording in the components, this package is bundled with the other packages when it's used and isn't released as a standalone
- `puik` contains all the other packages and is released under the name `@prestashopcorp/puik`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ defineProps<<%= h.changeCase.pascal(name) %>Props>();
<style lang="scss">
@use '@prestashopcorp/puik-theme/src/base.scss';
@use '@prestashopcorp/puik-theme/src/puik-<%= h.changeCase.param(name) %>.scss';
</style>
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
to: packages/web-components/components/<%= h.changeCase.param(name) %>.ts
---
import { defineCustomElement } from 'vue';
import { Puik<%= h.changeCase.pascal(name) %>} from '@prestashopcorp/puik-components';
import { Puik<%= h.changeCase.pascal(name) %> } from '@prestashopcorp/puik-components';
import type { CustomElementWithName } from '../types';

const Puik<%= h.changeCase.pascal(name) %>Ce = defineCustomElement(Puik<%= h.changeCase.pascal(name) %>) as CustomElementWithName;
Puik<%= h.changeCase.pascal(name) %>Ce.ceName = 'puik-<%= h.changeCase.param(name) %>-ce';

export default Puik<%= h.changeCase.pascal(name) %>Ce;
export default Puik<%= h.changeCase.pascal(name) %>Ce;
1 change: 1 addition & 0 deletions packages/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export * from './tag';
export * from './avatar';
export * from './divider';
export * from './notification-bar';
export * from './sortable-list';
5 changes: 4 additions & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
"@popperjs/core": "^2.11.8",
"@prestashopcorp/puik-theme": "workspace:*",
"@vueuse/core": "^10.9.0",
"radix-vue": "^1.7.4"
"radix-vue": "^1.7.4",
"sortablejs": "^1.15.2",
"sortablejs-vue3": "^1.2.11"
},
"devDependencies": {
"@types/sortablejs": "^1.15.8",
"vue": "^3.3.7",
"vue-router": "^4.3.2",
"vue-tsc": "^1.8.27"
Expand Down
6 changes: 6 additions & 0 deletions packages/components/sortable-list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import SortableList from './src/sortable-list.vue';

export const PuikSortableList = SortableList;
export default PuikSortableList;

export * from './src/sortable-list';
63 changes: 63 additions & 0 deletions packages/components/sortable-list/src/sortable-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import '@prestashopcorp/puik-components/sortable-list/style/css';
import type SortableList from './sortable-list.vue';
import Sortable, { SortableOptions } from 'sortablejs';
import type { AutoScrollOptions } from 'sortablejs/plugins';

export enum PuikSortableListIconPosition {
Left = 'left',
Right = 'right',
}

export enum PuikSortableListTag {
Menu = 'menu',
Ol = 'ol',
Ul = 'ul',
}

export type SortableOptionsProp = Omit<
SortableOptions | AutoScrollOptions,
| 'onUnchoose'
| 'onChoose'
| 'onStart'
| 'onEnd'
| 'onAdd'
| 'onUpdate'
| 'onSort'
| 'onRemove'
| 'onFilter'
| 'onMove'
| 'onClone'
| 'onChange'
>;

export type SortableEvent = Sortable.SortableEvent;
export type SortableMoveEvent = Sortable.MoveEvent;

export interface SortableListProps {
listId: string
list: any[]
displayPositionNumbers?: boolean
iconPosition?: `${PuikSortableListIconPosition}`
itemKey: string | ((item: any) => string | number | Symbol)
tag?: `${PuikSortableListTag}`
options?: any
dataTest?: string
}

export type SortableListEmits = {
(event: 'list-changed', evt: any[]): void
(event: 'choose', evt: Sortable.SortableEvent): void
(event: 'unchoose', evt: Sortable.SortableEvent): void
(event: 'start', evt: Sortable.SortableEvent): void
(event: 'end', evt: Sortable.SortableEvent): void
(event: 'add', evt: Sortable.SortableEvent): void
(event: 'update', evt: Sortable.SortableEvent): void
(event: 'sort', evt: Sortable.SortableEvent): void
(event: 'remove', evt: Sortable.SortableEvent): void
(event: 'filter', evt: Sortable.SortableEvent): void
(event: 'move', evt: Sortable.MoveEvent, originalEvent: Event): void
(event: 'clone', evt: Sortable.SortableEvent): void
(event: 'change', evt: Sortable.SortableEvent): void
};

export type SortableListInstance = InstanceType<typeof SortableList>;
243 changes: 243 additions & 0 deletions packages/components/sortable-list/src/sortable-list.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
<template>
<div
:id="listId"
:data-test="dataTest"
>
<Sortable
:ref="props.listId"
v-model:sortable="sortable"
:list="localList"
:item-key="props.itemKey"
:tag="props.tag"
:options="props.options"
@change="handleEvents"
@choose="handleEvents"
@unchoose="handleEvents"
@start="handleEvents"
@end="handleEvents"
@add="handleEvents"
@update="handleEvents"
@sort="handleEvents"
@remove="handleEvents"
@filter="handleEvents"
@move="handleMoveEvents"
@clone="handleEvents"
@keydown="handleKeyDown($event)"
>
<template #item="{ element, index }">
<li
:key="element"
:data-item="JSON.stringify(element)"
:class="['draggable', `draggable-${listId}`]"
tabindex="0"
:aria-label="`Item ${index + 1}`"
:data-sortable-id="index"
:data-test="`${dataTest}-item-${index + 1}`"
>
<div class="puik-sortable-list_item">
<span
v-if="displayPositionNumbers"
class="puik-sortable-list_item-index"
:data-test="`${dataTest}-list-item-index-${index + 1}`"
>
{{ $attrs.dataSortableId || index + 1 }}
</span>
<div class="puik-sortable-list_item-container">
<PuikIcon
v-if="iconPosition == PuikSortableListIconPosition.Left"
icon="drag_indicator"
color="#1D1D1B"
tabindex="-1"
:data-test="`${dataTest}-left-icon-${index + 1}`"
/>
<img
v-if="element.imgSrc"
class="puik-sortable-list_item-img"
:src="element.imgSrc"
alt="img alt"
:data-test="`${dataTest}-img-${index + 1}`"
>
<div class="puik-sortable-list_item-content">
<p
v-if="element.title"
class="puik-sortable-list_item-content_title"
:data-test="`${dataTest}-title-${index + 1}`"
>
{{ `${element?.title}` }}
</p>
<p
v-if="element.description"
class="puik-sortable-list_item-content_subtitle"
:data-test="`${dataTest}-description-${index + 1}`"
>
{{ `${element?.description}` }}
</p>
<slot
name="custom-content"
:element="element"
:index="index"
/>
</div>
<PuikIcon
v-if="iconPosition == PuikSortableListIconPosition.Right"
icon="drag_indicator"
color="#1D1D1B"
tabindex="-1"
:data-test="`${dataTest}-right-icon-${index + 1}`"
/>
</div>
</div>
</li>
</template>
</Sortable>
</div>
</template>
<script setup lang="ts">
import {
SortableListProps,
SortableListEmits,
SortableEvent,
SortableMoveEvent,
PuikSortableListIconPosition
, PuikSortableListTag
} from './sortable-list';
import { PuikIcon } from '@prestashopcorp/puik-components';
import { Sortable } from 'sortablejs-vue3';
import { nextTick, ref } from 'vue';
defineOptions({
name: 'PuikSortableList'
});
const props = withDefaults(defineProps<SortableListProps>(), {
tag: PuikSortableListTag.Ul,
iconPosition: PuikSortableListIconPosition.Right,
displayPositionNumbers: true,
options: { animation: 150 }
});
const emit = defineEmits<SortableListEmits>();
const sortable = ref<InstanceType<typeof Sortable> | null>(null);
defineExpose({
sortable
});
const localList = ref([...props.list]);
const handleMoveEvents = (evt: SortableMoveEvent, originalEvent: Event) => {
emit('move', evt, originalEvent);
};
const handleEvents = (event: SortableEvent) => {
let items: HTMLCollection;
if (event.type === 'remove') {
items = event.from.children;
} else {
items = event.to.children;
}
if (
['add', 'remove'].includes(event.type) ||
(event.type === 'end' && event.from.children === event.to.children)
) {
for (let i = 0; i < items.length; i++) {
items[i].setAttribute('data-sortable-id', i.toString());
const positionSpan = items[i].querySelector(
'.puik-sortable-list_item-index'
);
if (positionSpan) {
positionSpan.textContent = (i + 1).toString();
}
}
const newList = Array.from(items).map((item: { [key: string]: any }) => {
if (item.hasAttribute('data-item')) {
const dataItem = item.getAttribute('data-item');
return dataItem ? JSON.parse(dataItem) : null;
}
return null;
});
emit('list-changed', newList);
}
emit(event.type as keyof SortableListEmits, event);
};
let isProcessing = false;
const handleKeyDown = (event: KeyboardEvent) => {
event.preventDefault();
isProcessing = isProcessing ?? true;
if (props.options?.group === 'shared' || isProcessing) return;
const items = document.querySelectorAll(`.draggable-${props.listId}`);
for (let i = 0; i < items.length; i++) {
items[i].setAttribute('data-sortable-id', i.toString());
const positionSpan = items[i].querySelector(
'.puik-sortable-list_item-index'
);
if (positionSpan) {
positionSpan.textContent = (i + 1).toString();
}
}
const target = event.target as HTMLElement;
const key = event.key;
if (target.classList.contains(`draggable-${props.listId}`)) {
let newIndex: number | null = null;
const index = Number(target.getAttribute('data-sortable-id'));
if (event.shiftKey && (key === 'ArrowUp' || key === 'ArrowDown')) {
event.preventDefault();
if (key === 'ArrowUp' && index > 0) {
newIndex = index - 1;
} else if (key === 'ArrowDown' && index < localList.value.length - 1) {
newIndex = index + 1;
} else {
newIndex = index;
}
if (newIndex !== null) {
const itemToMove = localList.value[index];
localList.value.splice(index, 1);
localList.value.splice(newIndex, 0, itemToMove);
emit('list-changed', localList.value);
nextTick(() => {
const newTarget = document.querySelector(
`.draggable-${props.listId}[data-sortable-id="${newIndex}"]`
) as HTMLElement;
newTarget?.focus();
});
}
} else if (key === 'ArrowUp' || key === 'ArrowDown') {
event.preventDefault();
if (key === 'ArrowUp' && index > 0) {
newIndex = index - 1;
} else if (key === 'ArrowDown' && index < localList.value.length - 1) {
newIndex = index + 1;
} else {
newIndex = index;
}
if (newIndex !== null) {
const newTarget = document.querySelector(
`.draggable-${props.listId}[data-sortable-id="${newIndex}"]`
) as HTMLElement;
newTarget?.focus();
}
}
}
isProcessing = false;
};
</script>
<style lang="scss">
@use '@prestashopcorp/puik-theme/src/base.scss';
@use '@prestashopcorp/puik-theme/src/puik-sortable-list.scss';
@use '@prestashopcorp/puik-theme/src/puik-icon.scss';
</style>
Loading

0 comments on commit 84c05c5

Please sign in to comment.