@ouestfrance/sipa-bms-ui 8.15.1 → 8.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/components/layout/BmsFloatingWindow.vue.d.ts +19 -0
  2. package/dist/components/layout/BmsSplitWindow.vue.d.ts +35 -0
  3. package/dist/components/table/BmsTable.vue.d.ts +3 -1
  4. package/dist/components/table/UiBmsTable.vue.d.ts +1 -1
  5. package/dist/helpers/table.helper.d.ts +1 -0
  6. package/dist/index.d.ts +3 -1
  7. package/dist/mockServiceWorker.js +1 -1
  8. package/dist/models/table.model.d.ts +2 -1
  9. package/dist/sipa-bms-ui.css +168 -88
  10. package/dist/sipa-bms-ui.es.js +5086 -4115
  11. package/dist/sipa-bms-ui.es.js.map +1 -1
  12. package/dist/sipa-bms-ui.umd.js +5094 -4120
  13. package/dist/sipa-bms-ui.umd.js.map +1 -1
  14. package/package.json +19 -19
  15. package/src/components/form/BmsMultiSelect.vue +5 -1
  16. package/src/components/form/BmsServerAutocomplete.vue +13 -3
  17. package/src/components/layout/BmsFloatingWindow.stories.js +109 -0
  18. package/src/components/layout/BmsFloatingWindow.vue +83 -0
  19. package/src/components/layout/BmsModal.vue +1 -1
  20. package/src/components/layout/BmsSplitWindow.stories.js +155 -0
  21. package/src/components/layout/BmsSplitWindow.vue +290 -0
  22. package/src/components/table/BmsTable.stories.js +47 -0
  23. package/src/components/table/BmsTable.vue +22 -3
  24. package/src/components/table/UiBmsTable.spec.ts +47 -3
  25. package/src/components/table/UiBmsTable.stories.js +43 -0
  26. package/src/components/table/UiBmsTable.vue +51 -91
  27. package/src/components/table/UiBmsTableRow.vue +18 -3
  28. package/src/documentation/principles.mdx +73 -7
  29. package/src/helpers/table.helper.ts +19 -0
  30. package/src/index.ts +6 -0
  31. package/src/models/table.model.ts +1 -0
  32. package/src/showroom/pages/autocomplete.vue +2 -2
  33. package/src/showroom/pages/server-table.vue +2 -7
  34. package/src/showroom/pages/table.vue +3 -0
  35. package/src/showroom/server.js +19 -11
@@ -0,0 +1,290 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ onBeforeUnmount,
5
+ onMounted,
6
+ ref,
7
+ useTemplateRef,
8
+ watch,
9
+ } from 'vue';
10
+
11
+ const DEFAULT_MIN = 0;
12
+ const DEFAULT_MAX = 100;
13
+
14
+ interface Props {
15
+ splitOrientation?: 'horizontal' | 'vertical';
16
+ min?: number;
17
+ max?: number;
18
+ primary?: 'first' | 'second';
19
+ collapsed?: boolean;
20
+ ariaLabel?: string;
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ splitOrientation: 'vertical',
25
+ primary: 'first',
26
+ min: DEFAULT_MIN,
27
+ max: DEFAULT_MAX,
28
+ collapsable: false,
29
+ });
30
+
31
+ const emit = defineEmits(['update:collapsed']);
32
+
33
+ const split = defineModel<number>({
34
+ default: 50,
35
+ });
36
+
37
+ const container = useTemplateRef('split-window');
38
+ const primaryId = ref<string>(crypto.randomUUID());
39
+
40
+ //State Machine
41
+ const isDragging = ref<boolean>(false);
42
+ const startSplit = ref<number | null>(null);
43
+ const startPosition = ref<number | null>(null);
44
+
45
+ const min = computed(() =>
46
+ clamp(props.min ?? DEFAULT_MIN, DEFAULT_MIN, DEFAULT_MAX),
47
+ );
48
+ const max = computed(() =>
49
+ clamp(props.max ?? DEFAULT_MAX, DEFAULT_MIN, DEFAULT_MAX),
50
+ );
51
+
52
+ // Correction: état local pour collapsed
53
+ const collapsedLocal = ref(props.collapsed ?? false);
54
+
55
+ watch(
56
+ () => props.collapsed,
57
+ (val) => {
58
+ collapsedLocal.value = val ?? false;
59
+ },
60
+ );
61
+
62
+ function setCollapsed(val: boolean) {
63
+ collapsedLocal.value = val;
64
+ emit('update:collapsed', val);
65
+ }
66
+
67
+ const clampSplit = computed(() => {
68
+ if (collapsedLocal.value) {
69
+ return props.primary === 'first' ? min.value : max.value;
70
+ } else {
71
+ return clamp(split.value, min.value, max.value);
72
+ }
73
+ });
74
+
75
+ const size = computed(() => {
76
+ return `${clampSplit.value}fr auto ${100 - clampSplit.value}fr`;
77
+ });
78
+
79
+ const gridStyle = computed(() => {
80
+ return props.splitOrientation === 'horizontal'
81
+ ? {
82
+ gridTemplateRows: size.value,
83
+ gridTemplateColumns: 'none',
84
+ }
85
+ : {
86
+ gridTemplateColumns: size.value,
87
+ gridTemplateRows: 'none',
88
+ };
89
+ });
90
+
91
+ const HANDLED_KEYS = [
92
+ 'ArrowLeft',
93
+ 'ArrowRight',
94
+ 'ArrowUp',
95
+ 'ArrowDown',
96
+ 'Enter',
97
+ 'Home',
98
+ 'End',
99
+ ];
100
+
101
+ onMounted(() => {
102
+ window.addEventListener('pointermove', onPointerMove);
103
+ window.addEventListener('pointerup', onPointerUp);
104
+ });
105
+
106
+ onBeforeUnmount(() => {
107
+ window.removeEventListener('pointermove', onPointerMove);
108
+ window.removeEventListener('pointerup', onPointerUp);
109
+ });
110
+
111
+ function onPointerDown(evt: PointerEvent) {
112
+ isDragging.value = true;
113
+ setCollapsed(false);
114
+ startSplit.value = clampSplit.value;
115
+ startPosition.value =
116
+ props.splitOrientation === 'vertical' ? evt.clientX : evt.clientY;
117
+ }
118
+
119
+ function onPointerMove(evt: PointerEvent) {
120
+ if (!isDragging.value) return;
121
+ _move(evt);
122
+ }
123
+
124
+ function onPointerUp(evt: PointerEvent) {
125
+ if (!isDragging.value) return;
126
+ _move(evt);
127
+ isDragging.value = false;
128
+ startPosition.value = null;
129
+ startSplit.value = null;
130
+ }
131
+
132
+ function onKeyDown(evt: KeyboardEvent) {
133
+ if (
134
+ !HANDLED_KEYS.includes(evt.key) ||
135
+ (props.splitOrientation === 'horizontal' &&
136
+ (evt.key === 'ArrowLeft' || evt.key === 'ArrowRight')) ||
137
+ (props.splitOrientation === 'vertical' &&
138
+ (evt.key === 'ArrowUp' || evt.key === 'ArrowDown'))
139
+ ) {
140
+ return;
141
+ }
142
+
143
+ evt.preventDefault();
144
+ evt.stopPropagation();
145
+
146
+ switch (evt.key) {
147
+ case 'ArrowLeft':
148
+ case 'ArrowUp':
149
+ if (collapsedLocal.value === true) {
150
+ setCollapsed(false);
151
+ }
152
+ split.value = Math.max(min.value, clampSplit.value - 1);
153
+ break;
154
+ case 'ArrowRight':
155
+ case 'ArrowDown':
156
+ if (collapsedLocal.value === true) {
157
+ setCollapsed(false);
158
+ }
159
+ split.value = Math.min(max.value, clampSplit.value + 1);
160
+ break;
161
+ case 'Enter':
162
+ setCollapsed(!collapsedLocal.value);
163
+ break;
164
+ case 'Home':
165
+ if (collapsedLocal.value === true) {
166
+ setCollapsed(false);
167
+ }
168
+ split.value = props.primary === 'first' ? min.value : max.value;
169
+ break;
170
+ case 'End':
171
+ if (collapsedLocal.value === true) {
172
+ setCollapsed(false);
173
+ }
174
+ split.value = props.primary === 'first' ? max.value : min.value;
175
+ break;
176
+ }
177
+ }
178
+
179
+ function _move(evt: PointerEvent) {
180
+ if (startPosition.value === null || startSplit.value === null) return;
181
+
182
+ const currentPosition =
183
+ props.splitOrientation === 'vertical' ? evt.clientX : evt.clientY;
184
+
185
+ const delta = currentPosition - startPosition.value;
186
+
187
+ const containerSize =
188
+ props.splitOrientation === 'vertical'
189
+ ? container.value?.getBoundingClientRect().width || 1
190
+ : container.value?.getBoundingClientRect().height || 1;
191
+
192
+ const deltaPercent = (delta / containerSize) * 100;
193
+ split.value = startSplit.value + deltaPercent;
194
+ }
195
+
196
+ function clamp(value: number, minValue: number, maxValue: number) {
197
+ return Math.min(Math.max(value, minValue), maxValue);
198
+ }
199
+ </script>
200
+
201
+ <template>
202
+ <div
203
+ ref="split-window"
204
+ class="split-window"
205
+ :class="`split-window--${splitOrientation}`"
206
+ :style="gridStyle"
207
+ >
208
+ <div
209
+ class="split-window__first-pane"
210
+ :id="primary === 'first' ? primaryId : undefined"
211
+ >
212
+ <slot name="first" />
213
+ </div>
214
+ <div
215
+ class="split-window__separator"
216
+ role="separator"
217
+ tabindex="0"
218
+ :aria-label="props.ariaLabel || 'Séparateur de volet'"
219
+ :aria-valuemin="min"
220
+ :aria-valuemax="max"
221
+ :aria-valuenow="clampSplit"
222
+ :aria-orientation="splitOrientation"
223
+ :aria-controls="primaryId"
224
+ @pointerdown.prevent.stop="onPointerDown"
225
+ @keydown="onKeyDown"
226
+ />
227
+ <div
228
+ class="split-window__second-pane"
229
+ :id="primary === 'second' ? primaryId : undefined"
230
+ >
231
+ <slot name="second" />
232
+ </div>
233
+ </div>
234
+ </template>
235
+
236
+ <style scoped lang="scss">
237
+ .split-window {
238
+ display: grid;
239
+ width: 100%;
240
+ height: 100%;
241
+
242
+ &__separator {
243
+ position: relative;
244
+ z-index: 2;
245
+
246
+ &:before {
247
+ content: '';
248
+ position: absolute;
249
+ top: 0;
250
+ left: 0;
251
+ }
252
+
253
+ &:focus-within {
254
+ &:before {
255
+ outline: 2px solid black;
256
+ }
257
+ }
258
+ }
259
+
260
+ &--vertical {
261
+ .split-window__separator {
262
+ height: 100%;
263
+ width: 0;
264
+
265
+ &:before {
266
+ content: '';
267
+ width: 8px;
268
+ height: 100%;
269
+ transform: translate(-50%, 0);
270
+ cursor: col-resize;
271
+ }
272
+ }
273
+ }
274
+
275
+ &--horizontal {
276
+ .split-window__separator {
277
+ height: 0;
278
+ width: 100%;
279
+
280
+ &:before {
281
+ content: '';
282
+ width: 100%;
283
+ height: 8px;
284
+ transform: translate(0, -50%);
285
+ cursor: row-resize;
286
+ }
287
+ }
288
+ }
289
+ }
290
+ </style>
@@ -10,6 +10,12 @@ export default {
10
10
  disableSearch: {
11
11
  control: { type: 'boolean' },
12
12
  },
13
+ selectMode: {
14
+ options: ['single', 'default'],
15
+ },
16
+ mode: {
17
+ options: ['normal', 'dense'],
18
+ },
13
19
  loading: {
14
20
  control: { type: 'boolean' },
15
21
  },
@@ -751,6 +757,47 @@ Selected.args = {
751
757
  selectable: true,
752
758
  };
753
759
 
760
+ export const SingleSelected = Template.bind({});
761
+ SingleSelected.args = {
762
+ headers: [
763
+ {
764
+ label: 'Column 1',
765
+ key: 'col1',
766
+ align: 'start',
767
+ },
768
+ {
769
+ label: 'Column 2',
770
+ key: 'col2',
771
+ align: 'center',
772
+ },
773
+ {
774
+ label: 'Column 3',
775
+ key: 'col3',
776
+ align: 'end',
777
+ },
778
+ ],
779
+ items: [
780
+ {
781
+ col1: 'Value1',
782
+ col2: 'Value2',
783
+ col3: 'Value3',
784
+ },
785
+ {
786
+ col1: 'Value4',
787
+ col2: 'Value5',
788
+ col3: 'Value6',
789
+ },
790
+ ],
791
+ selectedItems: [
792
+ {
793
+ col1: 'Value1',
794
+ col2: 'Value2',
795
+ col3: 'Value3',
796
+ },
797
+ ],
798
+ selectable: true,
799
+ selectMode: 'single',
800
+ };
754
801
  const TemplateWithSelectAction = (args) => ({
755
802
  components: {
756
803
  BmsTable,
@@ -6,7 +6,14 @@ import UiFilterButton from '@/components/table/UiFilterButton.vue';
6
6
  import { usePagination } from '@/composables/pagination.composable';
7
7
  import { useSearch } from '@/composables/search.composable';
8
8
  import { useSort } from '@/composables/sort.composable';
9
- import { Filter, SavedFilter, Sort, SortValue, TableHeader } from '@/models';
9
+ import {
10
+ Filter,
11
+ SavedFilter,
12
+ SelectMode,
13
+ Sort,
14
+ SortValue,
15
+ TableHeader,
16
+ } from '@/models';
10
17
  import { computed, Ref, ref, watch, watchEffect } from 'vue';
11
18
  import BmsTableFilters from './BmsTableFilters.vue';
12
19
  import BmsSearch from '../form/BmsSearch.vue';
@@ -34,6 +41,7 @@ interface UiTableProps {
34
41
  defaultSort?: Sort;
35
42
  selectable?: boolean;
36
43
  selectableDisabled?: boolean;
44
+ selectMode?: SelectMode.DEFAULT | SelectMode.SINGLE;
37
45
  }
38
46
 
39
47
  const props = withDefaults(defineProps<UiTableProps>(), {
@@ -51,6 +59,7 @@ const props = withDefaults(defineProps<UiTableProps>(), {
51
59
  }),
52
60
  selectable: false,
53
61
  selectableDisabled: false,
62
+ selectMode: SelectMode.DEFAULT,
54
63
  });
55
64
 
56
65
  const {
@@ -212,9 +221,18 @@ const onClickHeader = (header: TableHeader) => {
212
221
  changeSort(header);
213
222
  };
214
223
 
224
+ const elementsAndChildElements = computed(() => {
225
+ const childElements = items.value
226
+ .map((item) => item.childElement)
227
+ .filter((childElement) => !!childElement);
228
+ return [...items.value, ...childElements];
229
+ });
230
+
215
231
  const onSelectAll = () => {
216
- selectedItems.value = items.value;
232
+ selectedItems.value = elementsAndChildElements.value;
217
233
  };
234
+
235
+ const totalSize = computed(() => elementsAndChildElements.value.length);
218
236
  </script>
219
237
 
220
238
  <template>
@@ -228,7 +246,8 @@ const onSelectAll = () => {
228
246
  :sort="sort"
229
247
  :selectable="selectable"
230
248
  :selectableDisabled="selectableDisabled"
231
- :totalSize="items.length"
249
+ :totalSize="totalSize"
250
+ :selectMode="selectMode"
232
251
  @clickHeader="onClickHeader"
233
252
  @selectAll="onSelectAll"
234
253
  >
@@ -2,9 +2,8 @@ import UiBmsTable from '@/components/table/UiBmsTable.vue';
2
2
  import { field } from '@/plugins/field';
3
3
  import { mount, MountingOptions } from '@vue/test-utils';
4
4
  import UiBmsInputCheckbox from '../form/UiBmsInputCheckbox.vue';
5
- import BmsAlert from '../feedback/BmsAlert.vue';
6
- import { wrap } from 'lodash';
7
5
  import { SelectMode } from '@/models';
6
+ import BmsInputRadio from '../form/BmsInputRadio.vue';
8
7
 
9
8
  const intersectionObserverMock = () => ({
10
9
  observe: () => null,
@@ -40,13 +39,21 @@ const factory = (options: MountingOptions<any, {}> = {}) => {
40
39
  } as any);
41
40
  return {
42
41
  wrapper,
42
+ selectedMessage: () => wrapper.findAll('.bms-table__selected')[0],
43
+
44
+ getAllCheckboxes: () => wrapper.findAllComponents(UiBmsInputCheckbox),
43
45
  getAllCheckedCheckboxes: () =>
44
46
  wrapper
45
47
  .findAllComponents(UiBmsInputCheckbox)
46
48
  .filter((checkbox) => checkbox.props('modelValue') === true),
47
49
  getTableCheckbox: () => wrapper.findAllComponents(UiBmsInputCheckbox)[0],
48
50
  getFirstRowCheckbox: () => wrapper.findAllComponents(UiBmsInputCheckbox)[1],
49
- selectedMessage: () => wrapper.findAll('.bms-table__selected')[0],
51
+
52
+ getAllCheckedRadio: () =>
53
+ wrapper
54
+ .findAllComponents(BmsInputRadio)
55
+ .filter((checkbox) => checkbox.props('modelValue') === true),
56
+ getAllRadio: () => wrapper.findAllComponents(BmsInputRadio),
50
57
  };
51
58
  };
52
59
  describe('UiBmsTable', () => {
@@ -369,5 +376,42 @@ describe('UiBmsTable', () => {
369
376
  'Vous avez sélectionné la totalité des 2 éléments.',
370
377
  );
371
378
  });
379
+ it('should display readio if in selectMode SINGLE', async () => {
380
+ const { getAllRadio, getAllCheckboxes } = factory({
381
+ props: {
382
+ headers,
383
+ items,
384
+ totalSize: 2,
385
+ selectable: true,
386
+ selectMode: SelectMode.SINGLE,
387
+ selectedItems: [items[0]],
388
+ },
389
+ });
390
+
391
+ expect(getAllRadio().length).toEqual(2);
392
+ expect(getAllCheckboxes().length).toEqual(0);
393
+ });
394
+ it('should unselect previous selection if in selectMode SINGLE', async () => {
395
+ const { getAllRadio } = factory({
396
+ props: {
397
+ headers,
398
+ items,
399
+ totalSize: 2,
400
+ selectable: true,
401
+ selectMode: SelectMode.SINGLE,
402
+ selectedItems: [items[0]],
403
+ },
404
+ });
405
+ const firstRadio = getAllRadio()[0];
406
+ const secondRadio = getAllRadio()[1];
407
+
408
+ expect(firstRadio.props('modelValue')).toBeTruthy();
409
+ expect(secondRadio.props('modelValue')).toBeFalsy();
410
+
411
+ await secondRadio.get('input').setValue(items[1]);
412
+
413
+ expect(secondRadio.props('modelValue')).toBeTruthy();
414
+ expect(firstRadio.props('modelValue')).toBeFalsy();
415
+ });
372
416
  });
373
417
  });
@@ -559,6 +559,49 @@ SelectedWithMaxSelected.args = {
559
559
  maxSelectedSize: 1,
560
560
  };
561
561
 
562
+ export const SingleSelect = Template.bind({});
563
+ SingleSelect.args = {
564
+ headers: [
565
+ {
566
+ label: 'Column 1',
567
+ key: 'col1',
568
+ align: 'start',
569
+ },
570
+ {
571
+ label: 'Column 2',
572
+ key: 'col2',
573
+ align: 'center',
574
+ },
575
+ {
576
+ label: 'Column 3',
577
+ key: 'col3',
578
+ align: 'end',
579
+ },
580
+ ],
581
+ items: [
582
+ {
583
+ col1: 'Value1',
584
+ col2: 'Value2',
585
+ col3: 'Value3',
586
+ },
587
+ {
588
+ col1: 'Value4',
589
+ col2: 'Value5',
590
+ col3: 'Value6',
591
+ },
592
+ ],
593
+ selectedItems: [
594
+ {
595
+ col1: 'Value1',
596
+ col2: 'Value2',
597
+ col3: 'Value3',
598
+ },
599
+ ],
600
+ selectable: true,
601
+ selectMode: 'single',
602
+ totalSize: 2,
603
+ };
604
+
562
605
  export const AllSelected = Template.bind({});
563
606
  AllSelected.args = {
564
607
  headers: [