diff --git a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts index d09e7209..09f5eaf6 100644 --- a/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts +++ b/packages/multiple-select-vanilla/src/MultipleSelectInstance.ts @@ -444,7 +444,7 @@ export class MultipleSelectInstance { const selectName = this.elm.getAttribute('name') || this.options.name || ''; this.selectAllParentElm = createDomElement('div', { className: 'ms-select-all', dataset: { key: 'select_all' } }); const saLabelElm = document.createElement('label'); - const saIconClass = this.isAllSelected ? 'ms-icon-check' : this.isPartiallyAllSelected ? 'ms-icon-minus' : 'ms-icon-uncheck'; + const saIconClass = this.isAllSelected ? 'ms-icon-check' : `ms-icon-${this.isPartiallyAllSelected ? 'partial-all' : 'uncheck'}`; const selectAllIconClass = `ms-icon ${saIconClass}`; const saIconContainerElm = createDomElement('div', { className: 'icon-checkbox-container' }, saLabelElm); createDomElement( @@ -635,6 +635,12 @@ export class MultipleSelectInstance { if (isSingleWithoutRadioIcon) { itemOrGroupBlock = inputCheckboxStruct; } else { + // determine if it's an optgroup and the group has a partial selection + let hasPartialGroupSelected = false; + if ('children' in dataRow && (dataRow as OptGroupRowData).children.some(child => child?.selected)) { + hasPartialGroupSelected = true; + } + itemOrGroupBlock = { tagName: 'div', props: { @@ -645,7 +651,7 @@ export class MultipleSelectInstance { { tagName: 'div', props: { - className: `ms-icon ${isChecked ? (type === 'radio' ? 'ms-icon-radio' : 'ms-icon-check') : 'ms-icon-uncheck'}`, + className: `ms-icon ${isChecked ? (type === 'radio' ? 'ms-icon-radio' : 'ms-icon-check') : `ms-icon-${hasPartialGroupSelected ? 'partial-group' : 'uncheck'}`}`, }, }, ], @@ -1575,6 +1581,11 @@ export class MultipleSelectInstance { const closestLiElm = inputElm.closest('li'); const iconDivElm = closestLiElm?.querySelector('.icon-checkbox-container div'); if (closestLiElm) { + // determine if it's an optgroup and the group has a partial selection + let hasPartialGroupSelected = false; + if ('children' in row && (row as OptGroupRowData).children.some(child => child?.selected)) { + hasPartialGroupSelected = true; + } if (row.selected && !closestLiElm.classList.contains('selected')) { closestLiElm.classList.add('selected'); closestLiElm.ariaSelected = 'true'; @@ -1585,7 +1596,7 @@ export class MultipleSelectInstance { closestLiElm.classList.remove('selected'); closestLiElm.ariaSelected = 'false'; if (iconDivElm) { - iconDivElm.className = 'ms-icon ms-icon-uncheck'; + iconDivElm.className = `ms-icon ms-icon-${hasPartialGroupSelected ? 'partial-group' : 'uncheck'}`; } } } @@ -1602,7 +1613,7 @@ export class MultipleSelectInstance { if (this.isAllSelected) { iconClass = 'ms-icon-check'; } else if (this.isPartiallyAllSelected) { - iconClass = 'ms-icon-minus'; + iconClass = 'ms-icon-partial-all'; } else { iconClass = 'ms-icon-uncheck'; } diff --git a/packages/multiple-select-vanilla/src/styles/_variables.scss b/packages/multiple-select-vanilla/src/styles/_variables.scss index 59fd9bbf..49782139 100644 --- a/packages/multiple-select-vanilla/src/styles/_variables.scss +++ b/packages/multiple-select-vanilla/src/styles/_variables.scss @@ -5,7 +5,7 @@ @use 'sass:color'; -// this is the only variable without $ms prefix and exists so that user could use +// this is the only variable without $ms prefix and exists so that user could use // the same Bootstrap primary color without declaring $ms-primary-color variable (which also exists) $primary-color: #149085 !default; $ms-primary-color: $primary-color !default; @@ -30,7 +30,8 @@ $ms-icon-caret-svg-path: "M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8 $ms-icon-close-svg-path: "M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" !default; $ms-icon-loading-svg-path: "M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" !default; $ms-icon-check-svg-path: "M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z" !default; -$ms-icon-minus-svg-path: "M20 14H4V10H20" !default; +$ms-icon-partial-all-svg-path: "M20 14H4V10H20" !default; +$ms-icon-partial-group-svg-path: "M19,13H5V11H19V13Z" !default; $ms-icon-radio-svg-path: "M12 3.7c4.6 0 8.3 3.7 8.3 8.3s-3.7 8.3-8.3 8.3-8.3-3.7-8.3-8.3S7.4 3.7 12 3.7z" !default; $ms-icon-color: #444 !default; $ms-icon-color-hover: #303030 !default; diff --git a/packages/multiple-select-vanilla/src/styles/multiple-select.scss b/packages/multiple-select-vanilla/src/styles/multiple-select.scss index cb080de0..b3cfe3d2 100644 --- a/packages/multiple-select-vanilla/src/styles/multiple-select.scss +++ b/packages/multiple-select-vanilla/src/styles/multiple-select.scss @@ -14,7 +14,8 @@ @include m.createSvgClass("ms-icon-caret", v.$ms-icon-caret-svg-path); @include m.createSvgClass("ms-icon-close", v.$ms-icon-close-svg-path); @include m.createSvgClass("ms-icon-check", v.$ms-icon-check-svg-path); -@include m.createSvgClass("ms-icon-minus", v.$ms-icon-minus-svg-path); +@include m.createSvgClass("ms-icon-partial-all", v.$ms-icon-partial-all-svg-path); +@include m.createSvgClass("ms-icon-partial-group", v.$ms-icon-partial-group-svg-path); @include m.createSvgClass("ms-icon-radio", v.$ms-icon-radio-svg-path); @include m.createSvgClass("ms-icon-loading", v.$ms-icon-loading-svg-path); @@ -56,14 +57,14 @@ width: var(--ms-checkbox-icon-container-width, v.$ms-checkbox-icon-container-width); border: var(--ms-checkbox-icon-container-border, v.$ms-checkbox-icon-container-border); border-radius: var(--ms-checkbox-icon-container-border-radius, v.$ms-checkbox-icon-container-border-radius); - + div { font-size: 14px; color: var(--ms-checkbox-color, v.$ms-checkbox-color); &:hover { color: var(--ms-checkbox-hover-color, v.$ms-checkbox-hover-color); } - // since we use the div container with a border, we don't actually need an icon for unchecked + // since we use the div container with a border, we don't actually need an icon for unchecked // BUT since we want to keep the same size, we can simply hide the mask to keep the same size &.ms-icon-uncheck { visibility: hidden; @@ -386,7 +387,7 @@ margin-top: var(--ms-drop-input-margin-top, v.$ms-drop-input-margin-top); accent-color: var(--ms-checkbox-color, v.$ms-checkbox-color); } - &:focus { + &:focus { outline: var(--ms-input-focus-outline, v.$ms-input-focus-outline); } } @@ -398,13 +399,13 @@ .ms-loading { display: flex; align-items: center; - gap: var(--ms-loading-gap, v.$ms-loading-gap); + gap: var(--ms-loading-gap, v.$ms-loading-gap); padding: var(--ms-loading-padding, v.$ms-loading-padding); .ms-icon-loading { font-size: var(--ms-loading-icon-size, v.$ms-loading-icon-size); height: var(--ms-loading-icon-size, v.$ms-loading-icon-size); width: var(--ms-loading-icon-size, v.$ms-loading-icon-size); - } + } } .ms-infinite-option { diff --git a/playwright/e2e/example03.spec.ts b/playwright/e2e/example03.spec.ts index 6cf0a7a5..b54548f5 100644 --- a/playwright/e2e/example03.spec.ts +++ b/playwright/e2e/example03.spec.ts @@ -26,10 +26,16 @@ test.describe('Example 03 - Multiple Width', () => { test('second select and expect optgroup selection to select the entire group when optgroup is selected', async ({ page }) => { await page.goto('#/example03'); await page.locator('div[data-test=select2].ms-parent').click(); + let group1SelectAll = await page.locator('[data-key="group_0"] .icon-checkbox-container div'); + await expect(group1SelectAll).toHaveClass('ms-icon ms-icon-uncheck'); await page.getByText('Group 1').click(); + group1SelectAll = await page.locator('[data-key="group_0"] .icon-checkbox-container div'); + await expect(group1SelectAll).toHaveClass('ms-icon ms-icon-check'); await page.getByRole('button', { name: '5 of 15 selected' }).click(); await page.getByRole('button', { name: '5 of 15 selected' }).click(); await page.getByRole('option', { name: '3', exact: true }).click(); + group1SelectAll = await page.locator('[data-key="group_0"] .icon-checkbox-container div'); + await expect(group1SelectAll).toHaveClass('ms-icon ms-icon-partial-group'); expect(await page.getByRole('option', { name: '3', exact: true }).locator('div').nth(1)).toHaveClass('ms-icon ms-icon-uncheck'); await page.getByRole('button', { name: '4 of 15 selected' }).click(); await page.getByRole('button', { name: '4 of 15 selected' }).click(); @@ -42,7 +48,7 @@ test.describe('Example 03 - Multiple Width', () => { await page.getByRole('button', { name: '14 of 15 selected' }).click(); await page.getByRole('button', { name: '14 of 15 selected' }).click(); const selectAll2 = await page.locator('[data-test=select2] .ms-select-all .icon-checkbox-container div'); - await expect(selectAll2).toHaveClass('ms-icon ms-icon-minus'); + await expect(selectAll2).toHaveClass('ms-icon ms-icon-partial-all'); await page.getByRole('option', { name: '3', exact: true }).click(); expect(await page.getByRole('option', { name: '3', exact: true }).locator('div').nth(1)).toHaveClass('ms-icon ms-icon-check'); await expect(selectAll2).toHaveClass('ms-icon ms-icon-check'); diff --git a/playwright/e2e/example06.spec.ts b/playwright/e2e/example06.spec.ts index 2a372e3b..69b66fa1 100644 --- a/playwright/e2e/example06.spec.ts +++ b/playwright/e2e/example06.spec.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; test.describe('Example 06 - Disabled items', () => { test('first select disabled selection February, March are shown but not clickable', async ({ page }) => { @@ -14,11 +14,19 @@ test.describe('Example 06 - Disabled items', () => { test('second select disabled group selection Group1, Option1,2,3 should not be toggable', async ({ page }) => { await page.goto('#/example06'); await page.getByRole('button', { name: '[Group 1: Option 1], [Group 2: Option 5]' }).click(); + const group1SelectAll = await page.locator('[data-key="group_0"] .icon-checkbox-container div'); + await expect(group1SelectAll).toHaveClass('ms-icon ms-icon-partial-group'); + let group2SelectAll = await page.locator('[data-key="group_1"] .icon-checkbox-container div'); + await expect(group2SelectAll).toHaveClass('ms-icon ms-icon-partial-group'); await page.getByRole('option', { name: 'Group 2' }).click(); + group2SelectAll = await page.locator('[data-key="group_1"] .icon-checkbox-container div'); + await expect(group2SelectAll).toHaveClass('ms-icon ms-icon-check'); await page.getByRole('button', { name: '[Group 1: Option 1], [Group 2: Option 5]' }); await page.getByRole('button', { name: '4 of 9 selected' }).click(); await page.getByRole('button', { name: '4 of 9 selected' }).click(); await page.locator('label').filter({ hasText: 'Option 4' }).click(); await page.getByRole('button', { name: '[Group 1: Option 1], [Group 2: Option 5, Option 6]' }).click(); + const group3SelectAll = await page.locator('[data-key="group_2"] .icon-checkbox-container div'); + await expect(group3SelectAll).toHaveClass('ms-icon ms-icon-uncheck'); }); });