Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[new-component] sortable list #365

Merged
merged 26 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5601587
feat: [Sortable-list] wip
mattgoud Jun 25, 2024
c41ff56
feat: [Sortable-list] keyboard accessibility
mattgoud Jun 26, 2024
4afb2cb
feat: [Sortable-list] handle events (wip)
mattgoud Jun 27, 2024
b5e6af8
feat: [Sortable-list] wip
mattgoud Jun 25, 2024
19512c4
feat: [Sortable-list] keyboard accessibility
mattgoud Jun 26, 2024
acc1722
feat: [Sortable-list] handle events (wip)
mattgoud Jun 27, 2024
22c5a8c
Merge branch '42-component-sortable-list' of github.com:PrestaShopCor…
mattgoud Jul 17, 2024
f8f104e
feat: [sortable-list] - handle add/remove/end events, create story, a…
mattgoud Jul 19, 2024
8361e66
feat: [sortable-list] - related#364 : add slot to handle custom list…
mattgoud Jul 23, 2024
f8b1906
feat: [sortable-list] - related #364 : update stories, add tests (wip)
mattgoud Jul 24, 2024
d2184a6
feat: [sortable-list] - handle move events, add sortable js types fil…
mattgoud Jul 25, 2024
f1a31d6
feat: [sortable-list] - related #324 - stories (wip)
mattgoud Jul 25, 2024
5125d9d
chore: merge main
mattgoud Jul 26, 2024
faf4bcb
Merge branch 'main' into 42-component-sortable-list
mattgoud Jul 29, 2024
59a04a5
feat: [Sortable-list] fix #364 : refacto tests and stories
mattgoud Jul 31, 2024
168329c
feat: [sortable-list] - related #364 : add dataTest prop (docs and t…
mattgoud Sep 5, 2024
f4f0506
feat: add data-test on html elements
guillaume60240 Sep 6, 2024
633ac2f
Merge branch 'main' into 42-component-sortable-list
guillaume60240 Sep 6, 2024
fb6ce54
feat: test data-test props. Warning, failed on iconPosition
guillaume60240 Sep 9, 2024
08330ef
fix: data-test test
guillaume60240 Sep 9, 2024
e0e6a7f
fix: storyBook
guillaume60240 Sep 9, 2024
4febc27
fix: enum for tag and lint:fix
guillaume60240 Sep 9, 2024
a6d03e8
fix: story book
guillaume60240 Sep 9, 2024
d0b8147
fix: re storybook, add options
guillaume60240 Sep 9, 2024
3ff28b4
fix: re options
guillaume60240 Sep 9, 2024
16a3e99
Merge branch 'main' into 42-component-sortable-list
mattgoud Sep 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading