From 854bea70902464013cbcbdc7a5bc952c733a4a17 Mon Sep 17 00:00:00 2001 From: Chamara Abesinghe Date: Wed, 22 Nov 2023 18:37:32 +0530 Subject: [PATCH] Fix OXD list table highlighting issue (#738) * TEC-165: Fix OXD list table highlighting issue * TEC-165: Add clickable, localization cells to OXD list table * TEC-165: Fix focus first directive to exclude disabled inputs --- changelog.md | 4 + components/jest.config.js | 1 + components/jest.init.js | 5 + .../composables/__tests__/useFlashing.spec.ts | 108 ++++++++++++++++++ components/src/composables/useFlashing.ts | 32 +++--- .../components/ListTable/CellContainer.vue | 5 + .../components/ListTable/ClickableCell.vue | 77 +++++++++++++ .../core/components/ListTable/ListTable.vue | 1 + .../components/ListTable/LocalizedCell.vue | 41 +++++++ .../directives/focus-first-element/index.ts | 3 +- yarn.lock | 6 +- 11 files changed, 263 insertions(+), 20 deletions(-) create mode 100644 components/jest.init.js create mode 100644 components/src/composables/__tests__/useFlashing.spec.ts create mode 100644 components/src/core/components/ListTable/ClickableCell.vue create mode 100644 components/src/core/components/ListTable/LocalizedCell.vue diff --git a/changelog.md b/changelog.md index d2f94aa73..68750d1fd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +2023-11-21 - 35904535e09b3a0c2048300b53e0345f6f00ec85 - directives/focus-first-element - Fix focus on disabled input elements + +2023-11-21 - 7bdeb5b38f87223920e3acc074b0e5b1062d38d8 - ListTable/ListTable.vue - Add new cell types, fixed duplicated key warnings + 2023-11-20 - b2baf9c07983d0bdf9c772786f0dbd9b68b7172b - Icon/icons.ts - Add oxd-window-in, oxd-window-out icons 2023-11-18 - 37b5c67d67b24357d773210c952d3a899d60b008 - onKeypress changed to skip the disabled options when updating the pointer in navigation-mixin. diff --git a/components/jest.config.js b/components/jest.config.js index 6ab07989e..dc42a8bdc 100644 --- a/components/jest.config.js +++ b/components/jest.config.js @@ -7,4 +7,5 @@ module.exports = { moduleNameMapper: { '^!!raw-loader!(.*)$': '$1', }, + setupFiles: ['/jest.init.js'], }; diff --git a/components/jest.init.js b/components/jest.init.js new file mode 100644 index 000000000..17cc6580d --- /dev/null +++ b/components/jest.init.js @@ -0,0 +1,5 @@ +import {config} from '@vue/test-utils'; + +config.global.mocks = { + $t: text => text, +}; diff --git a/components/src/composables/__tests__/useFlashing.spec.ts b/components/src/composables/__tests__/useFlashing.spec.ts new file mode 100644 index 000000000..a7b304d90 --- /dev/null +++ b/components/src/composables/__tests__/useFlashing.spec.ts @@ -0,0 +1,108 @@ +import {defineComponent} from 'vue'; +import {mount} from '@vue/test-utils'; +import useFlashing, {getDiff} from '../useFlashing'; + +const TestComponent = defineComponent({ + props: ['loading', 'flashing', 'items', 'order'], + setup(props, context) { + return { + flashIndexes: useFlashing(props, context), + }; + }, + template: `
{{flashIndexes}}
`, +}); + +describe('useFlashing composable', () => { + it('useFlashing::getDiff should detect when item added to array', () => { + const original = [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + ]; + + const updated = [...original, {id: 4, name: 'Banana'}]; + expect(getDiff(updated, original)).toStrictEqual([3]); + }); + it('useFlashing::getDiff should detect when item updated in array', () => { + const original = [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + ]; + + const updated = JSON.parse(JSON.stringify(original)); + updated[2].name = 'Guava'; + expect(getDiff(updated, original)).toStrictEqual([2]); + }); + it('useFlashing::getDiff should not detect when items reordered in array', () => { + const original = [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + ]; + + const updated = [...original].reverse(); + expect(getDiff(updated, original)).toStrictEqual([]); + }); + it('useFlashing::getDiff should not detect when items removed in array', () => { + const original = [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + ]; + + const updated = [...original]; + updated.pop(); + expect(getDiff(updated, original)).toStrictEqual([]); + }); + + it('useFlashing should detect changes', async () => { + const wrapper = mount(TestComponent, { + props: { + flashing: true, + }, + }); + await wrapper.setProps({ + items: [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + ], + }); + expect(wrapper.vm.flashIndexes).toStrictEqual([]); + await wrapper.setProps({ + items: [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + {id: 4, name: 'Guava'}, + ], + }); + expect(wrapper.vm.flashIndexes).toStrictEqual([3]); + }); + + it('useFlashing should not detect changes if flashing disabled', async () => { + const wrapper = mount(TestComponent, { + props: { + flashing: false, + }, + }); + await wrapper.setProps({ + items: [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + ], + }); + expect(wrapper.vm.flashIndexes).toStrictEqual([]); + await wrapper.setProps({ + items: [ + {id: 1, name: 'Orange'}, + {id: 2, name: 'Apple'}, + {id: 3, name: 'Cherry'}, + {id: 4, name: 'Guava'}, + ], + }); + expect(wrapper.vm.flashIndexes).toStrictEqual([]); + }); +}); diff --git a/components/src/composables/useFlashing.ts b/components/src/composables/useFlashing.ts index 970d37a07..10fdf224c 100644 --- a/components/src/composables/useFlashing.ts +++ b/components/src/composables/useFlashing.ts @@ -4,6 +4,20 @@ type RowItem = { [name: string]: string | number | object | null; }; +export const getDiff = (newValue: RowItem[], oldValue: RowItem[]) => { + const diff: number[] = []; + for (let i = 0; i < newValue.length; i++) { + const isNew = + oldValue.findIndex(item => { + const o = JSON.stringify(Object.entries(item).sort()); + const n = JSON.stringify(Object.entries(newValue[i]).sort()); + return o === n; + }) === -1; + if (isNew) diff.push(i); + } + return diff; +}; + export default function useFlashing( props: ShallowReactive<{ order: object; @@ -13,9 +27,9 @@ export default function useFlashing( }>, context: SetupContext, ) { - const flashIndexes = shallowRef([]); let flash = false; - let cachedItems: RowItem[] = JSON.parse(JSON.stringify(props.items)); + let cachedItems: RowItem[] = []; + const flashIndexes = shallowRef([]); // Reset cached values if loading or sorting watch( @@ -31,20 +45,6 @@ export default function useFlashing( }, ); - const getDiff = (newValue: RowItem[], oldValue: RowItem[]) => { - const diff: number[] = []; - for (let i = 0; i < newValue.length; i++) { - const isNew = - oldValue.findIndex(item => { - const o = JSON.stringify(Object.entries(item).sort()); - const n = JSON.stringify(Object.entries(newValue[i]).sort()); - return o === n; - }) === -1; - if (isNew) diff.push(i); - } - return diff; - }; - if (props.flashing) { watch( () => props.items, diff --git a/components/src/core/components/ListTable/CellContainer.vue b/components/src/core/components/ListTable/CellContainer.vue index e6189c592..1c8643721 100755 --- a/components/src/core/components/ListTable/CellContainer.vue +++ b/components/src/core/components/ListTable/CellContainer.vue @@ -8,6 +8,8 @@ import { resolveComponent, } from 'vue'; import DefaultCell from './Default.vue'; +import ClickableCell from './ClickableCell.vue'; +import LocalizedCell from './LocalizedCell.vue'; import {CardHeaders} from '../CardTable/types'; import {RowItem} from '../CardTable/Cell/types'; import LinkCell from '@orangehrm/oxd/core/components/CardTable/Cell/Link.vue'; @@ -27,6 +29,8 @@ export default defineComponent({ 'oxd-table-cell-default': DefaultCell, 'oxd-table-cell-actions': ActionsCell, 'oxd-table-cell-checkbox': CheckboxCell, + 'oxd-table-cell-clickable': ClickableCell, + 'oxd-table-cell-localized': LocalizedCell, 'oxd-table-cell-profile-pic': ProfilePicCell, 'oxd-table-cell-link-with-pill': LinkWithPillCell, }, @@ -84,6 +88,7 @@ export default defineComponent({ item: cellData, header: header, rowItem: rowData, + class: header.class, loading: this.loading, }, header.cellProps ?? {}, diff --git a/components/src/core/components/ListTable/ClickableCell.vue b/components/src/core/components/ListTable/ClickableCell.vue new file mode 100644 index 000000000..a1b7eecc3 --- /dev/null +++ b/components/src/core/components/ListTable/ClickableCell.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/components/src/core/components/ListTable/ListTable.vue b/components/src/core/components/ListTable/ListTable.vue index fc29ea9cb..bbe7cc5d3 100644 --- a/components/src/core/components/ListTable/ListTable.vue +++ b/components/src/core/components/ListTable/ListTable.vue @@ -272,6 +272,7 @@ export default defineComponent({ _skeleton: true, }; props.headers.forEach(header => { + defaultValue['_id'] = nanoid(); defaultValue[header.name] = null; }); diff --git a/components/src/core/components/ListTable/LocalizedCell.vue b/components/src/core/components/ListTable/LocalizedCell.vue new file mode 100644 index 000000000..49a4c6da9 --- /dev/null +++ b/components/src/core/components/ListTable/LocalizedCell.vue @@ -0,0 +1,41 @@ + + + diff --git a/components/src/directives/focus-first-element/index.ts b/components/src/directives/focus-first-element/index.ts index 2b772ccbd..8b130211e 100644 --- a/components/src/directives/focus-first-element/index.ts +++ b/components/src/directives/focus-first-element/index.ts @@ -4,7 +4,8 @@ export interface FocusFirstHTMLElement extends HTMLElement { activeElement?: Element | null; } -const focusableElements = 'input, select, textarea, [tabindex], [href]'; +const focusableElements = + 'input:not([disabled]), select, textarea, [tabindex], [href]'; const excludeElements = 'button:not(.oxd-dialog-close-button,.modal-reset-button,[disabled])'; const firstFocusedElementsOnMounted = new Map(); diff --git a/yarn.lock b/yarn.lock index 1f2746a43..da54cf807 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4696,9 +4696,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001286: - version "1.0.30001486" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz" - integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg== + version "1.0.30001563" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001563.tgz" + integrity sha512-na2WUmOxnwIZtwnFI2CZ/3er0wdNzU7hN+cPYz/z2ajHThnkWjNBOpEPP4n+4r2WPM847JaMotaJE3bnfzjyKw== capture-exit@^2.0.0: version "2.0.0"