diff --git a/.changeset/short-nails-attend.md b/.changeset/short-nails-attend.md new file mode 100644 index 0000000000..2bb3e9c680 --- /dev/null +++ b/.changeset/short-nails-attend.md @@ -0,0 +1,5 @@ +--- +'@sl-design-system/announcer': minor +--- + +Added a `force` option to the `announce()` function to allow repeated announcements of the same message (e.g. when re-focusing an active row in the grid). diff --git a/.changeset/vast-wombats-cut.md b/.changeset/vast-wombats-cut.md new file mode 100644 index 0000000000..b3530863cb --- /dev/null +++ b/.changeset/vast-wombats-cut.md @@ -0,0 +1,11 @@ +--- +'@sl-design-system/grid': patch +--- + +Accessibility improvements for row activation and selection: + +- Added `aria-current` to the active/selected row in activate and single-select modes. +- The grid now announces row activation and deactivation to screen readers. +- When you focus an already active row with the keyboard, the grid reannounces it (using `force`). + +**Note:** If you use a button to trigger row activation, you should add `aria-pressed` and `aria-description` to it yourself. The grid does not set these for you. See the `'Activate'` story for an example of how to do this. diff --git a/packages/components/announcer/src/announce.ts b/packages/components/announcer/src/announce.ts index 33645197ff..6505aa9942 100644 --- a/packages/components/announcer/src/announce.ts +++ b/packages/components/announcer/src/announce.ts @@ -6,7 +6,10 @@ * * @param message - The message to send to the live aria. * @param urgency - The urgency of the message. Default is 'polite'. + * @param force - If true, bypasses deduplication and always announces the message. */ -export function announce(message: string, urgency?: 'polite' | 'assertive'): void { - document.body.dispatchEvent(new CustomEvent('sl-announce', { detail: { message, urgency } })); +export function announce(message: string, urgency?: 'polite' | 'assertive', force?: boolean): void { + document.body.dispatchEvent( + new CustomEvent('sl-announce', { detail: { message, urgency, ...(force ? { force } : {}) } }) + ); } diff --git a/packages/components/announcer/src/announcer.ts b/packages/components/announcer/src/announcer.ts index 17ee93d0b8..216b4319a1 100644 --- a/packages/components/announcer/src/announcer.ts +++ b/packages/components/announcer/src/announcer.ts @@ -13,7 +13,11 @@ declare global { } } -export type SlAnnounceEvent = CustomEvent<{ message: string; urgency?: 'polite' | 'assertive' }>; +export type SlAnnounceEvent = CustomEvent<{ + message: string; + urgency?: 'polite' | 'assertive'; + force?: boolean; +}>; /** * Utility that serves as a recipient for all live-aria notifications and supplies them for @@ -30,6 +34,9 @@ export class Announcer extends LitElement { #events = new EventsController(this, {}); + /** Counter used to make forced announcements unique for screen reader deduplication. */ + #forceCounter = 0; + override connectedCallback(): void { super.connectedCallback(); @@ -48,15 +55,24 @@ export class Announcer extends LitElement { `[aria-live="${event.detail.urgency || 'polite'}"]` ); - // make sure the message is not already in the container - if (container?.textContent?.indexOf(event.detail.message) === -1) { - const messageNode = document.createElement('li'); - messageNode.innerText = event.detail.message; + const messageNode = document.createElement('li'); - container?.appendChild(messageNode); - setTimeout(() => { - messageNode.remove(); - }, 500); + if (event.detail.force) { + this.#forceCounter++; + // Append invisible zero width spaces to make each message unique for screen readers. + // We use % 4 so the suffix cycles through 1 – 4 characters, which is enough to avoid + // duplicates while messages are still in the live region (removed after 500ms). + messageNode.innerText = event.detail.message + '\u200B'.repeat((this.#forceCounter % 4) + 1); + } else if (container?.textContent?.indexOf(event.detail.message) === -1) { + // make sure the message is not already in the container + messageNode.innerText = event.detail.message; + } else { + return; } + + container?.appendChild(messageNode); + setTimeout(() => { + messageNode.remove(); + }, 500); } } diff --git a/packages/components/grid/package.json b/packages/components/grid/package.json index 202ec017f8..5b5c852811 100644 --- a/packages/components/grid/package.json +++ b/packages/components/grid/package.json @@ -40,6 +40,7 @@ "test": "echo \"Error: run tests from monorepo root.\" && exit 1" }, "dependencies": { + "@sl-design-system/announcer": "^0.0.8", "@sl-design-system/button": "^2.1.0", "@sl-design-system/checkbox": "^2.1.10", "@sl-design-system/data-source": "^0.4.0", diff --git a/packages/components/grid/src/grid.spec.ts b/packages/components/grid/src/grid.spec.ts index c376c62c68..871f1329d6 100644 --- a/packages/components/grid/src/grid.spec.ts +++ b/packages/components/grid/src/grid.spec.ts @@ -105,6 +105,14 @@ describe('sl-grid', () => { expect(rowIndices).to.deep.equal(['1', '2']); }); + + it('should not have aria-current when no row action or selection is configured', () => { + const rows = el.renderRoot.querySelectorAll('tbody tr'); + + rows.forEach(row => { + expect(row).not.to.have.attribute('aria-current'); + }); + }); }); describe('multiple select', () => { @@ -418,6 +426,19 @@ describe('sl-grid', () => { expect(el.dataSource?.selects).to.equal('single'); }); + it('should set aria-current="true" on the selected row', async () => { + el.renderRoot + .querySelector('tbody tr:first-of-type td:last-of-type') + ?.click(); + await new Promise(resolve => setTimeout(resolve)); + + const row = el.renderRoot.querySelector('tbody tr:first-of-type'), + otherRow = el.renderRoot.querySelector('tbody tr:nth-of-type(2)'); + + expect(row).to.have.attribute('aria-current', 'true'); + expect(otherRow).not.to.have.attribute('aria-current'); + }); + it('should toggle the "selected" part of the row when clicking in the row', async () => { el.renderRoot .querySelector('tbody tr:first-of-type td:last-of-type') @@ -429,7 +450,6 @@ describe('sl-grid', () => { }); it('should allow only one row to be selected at a time', async () => { - // Select first row el.renderRoot .querySelector('tbody tr:first-of-type td:last-of-type') ?.click(); @@ -440,7 +460,6 @@ describe('sl-grid', () => { ); expect(selectedRows).to.have.lengthOf(1); - // Select second row - should deselect first row el.renderRoot .querySelector('tbody tr:nth-of-type(2) td:last-of-type') ?.click(); @@ -451,17 +470,14 @@ describe('sl-grid', () => { ); expect(selectedRows).to.have.lengthOf(1); - // Verify first row is no longer selected const firstRow = el.renderRoot.querySelector('tbody tr:first-of-type'); expect(firstRow?.part.contains('selected')).to.be.false; - // Verify second row is selected const secondRow = el.renderRoot.querySelector('tbody tr:nth-of-type(2)'); expect(secondRow?.part.contains('selected')).to.be.true; }); it('should deselect a row when clicking it again', async () => { - // Select a row el.renderRoot .querySelector('tbody tr:first-of-type td:last-of-type') ?.click(); @@ -470,7 +486,6 @@ describe('sl-grid', () => { let row = el.renderRoot.querySelector('tbody tr:first-of-type'); expect(row?.part.contains('selected')).to.be.true; - // Click again to deselect el.renderRoot .querySelector('tbody tr:first-of-type td:last-of-type') ?.click(); @@ -586,6 +601,105 @@ describe('sl-grid', () => { expect(onActiveRowChange.firstCall.args[0].detail).to.deep.equal(el.items!.at(1)); }); + it('should set aria-current="true" on the active row', async () => { + el.renderRoot.querySelector('tbody tr:last-of-type')?.click(); + await new Promise(resolve => setTimeout(resolve)); + + const row = el.renderRoot.querySelector('tbody tr:last-of-type'), + otherRow = el.renderRoot.querySelector('tbody tr:first-of-type'); + + expect(row).to.have.attribute('aria-current', 'true'); + expect(otherRow).not.to.have.attribute('aria-current'); + }); + + it('should remove aria-current when deactivating', async () => { + el.renderRoot.querySelector('tbody tr:last-of-type')?.click(); + await new Promise(resolve => setTimeout(resolve)); + + let row = el.renderRoot.querySelector('tbody tr:last-of-type'); + + expect(row).to.have.attribute('aria-current', 'true'); + + row?.click(); + await new Promise(resolve => setTimeout(resolve)); + + row = el.renderRoot.querySelector('tbody tr:last-of-type'); + + expect(row).not.to.have.attribute('aria-current'); + }); + + it('should dispatch sl-announce event when activating a row', async () => { + const announceSpy = spy(); + document.body.addEventListener('sl-announce', announceSpy); + + el.renderRoot.querySelector('tbody tr:last-of-type')?.click(); + await new Promise(resolve => setTimeout(resolve)); + + expect(announceSpy).to.have.been.calledOnce; + + const event = announceSpy.firstCall.args[0] as CustomEvent<{ + message: string; + urgency: string; + }>; + + expect(event.detail.message).to.equal('Row 3 activated'); + expect(event.detail.urgency).to.equal('polite'); + + document.body.removeEventListener('sl-announce', announceSpy); + }); + + it('should dispatch sl-announce event when deactivating a row', async () => { + el.renderRoot.querySelector('tbody tr:last-of-type')?.click(); + await new Promise(resolve => setTimeout(resolve)); + + const announceSpy = spy(); + document.body.addEventListener('sl-announce', announceSpy); + el.renderRoot.querySelector('tbody tr:last-of-type')?.click(); + await new Promise(resolve => setTimeout(resolve)); + + expect(announceSpy).to.have.been.calledOnce; + + const event = announceSpy.firstCall.args[0] as CustomEvent<{ + message: string; + urgency: string; + }>; + + expect(event.detail.message).to.equal('Row 3 deactivated'); + expect(event.detail.urgency).to.equal('polite'); + + document.body.removeEventListener('sl-announce', announceSpy); + }); + + it('should dispatch sl-announce with force=true when focusing into an active row', async () => { + el.renderRoot.querySelector('tbody tr:first-of-type')?.click(); + await new Promise(resolve => setTimeout(resolve)); + + const tbody = el.renderRoot.querySelector('tbody')!; + tbody.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + await new Promise(resolve => setTimeout(resolve)); + + const announceSpy = spy(); + document.body.addEventListener('sl-announce', announceSpy); + + const td = el.renderRoot.querySelector('tbody tr:first-of-type td'); + td?.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); + await new Promise(resolve => setTimeout(resolve)); + + expect(announceSpy).to.have.been.calledOnce; + + const event = announceSpy.firstCall.args[0] as CustomEvent<{ + message: string; + urgency: string; + force: boolean; + }>; + + expect(event.detail.message).to.equal('In activated row 2'); + expect(event.detail.urgency).to.equal('assertive'); + expect(event.detail.force).to.be.true; + + document.body.removeEventListener('sl-announce', announceSpy); + }); + it('should keep sticky active row cells opaque', async () => { el = await fixture(html` { expect(toggleSpy).to.have.been.calledOnce; expect(toggleSpy.firstCall.args[0]).to.have.property('data', el.items?.at(0)); }); + + it('should dispatch sl-announce event when selecting a row', async () => { + const announceSpy = spy(); + document.body.addEventListener('sl-announce', announceSpy); + + el.renderRoot + .querySelector('tbody tr:first-of-type td:last-of-type') + ?.click(); + await new Promise(resolve => setTimeout(resolve)); + + expect(announceSpy).to.have.been.calledOnce; + + const event = announceSpy.firstCall.args[0] as CustomEvent<{ + message: string; + urgency: string; + }>; + expect(event.detail.message).to.equal('Row 2 activated'); + expect(event.detail.urgency).to.equal('polite'); + + document.body.removeEventListener('sl-announce', announceSpy); + }); + + it('should dispatch sl-announce event when deselecting a row', async () => { + el.renderRoot + .querySelector('tbody tr:first-of-type td:last-of-type') + ?.click(); + await new Promise(resolve => setTimeout(resolve)); + + const announceSpy = spy(); + document.body.addEventListener('sl-announce', announceSpy); + + el.renderRoot + .querySelector('tbody tr:first-of-type td:last-of-type') + ?.click(); + await new Promise(resolve => setTimeout(resolve)); + + expect(announceSpy).to.have.been.calledOnce; + + const event = announceSpy.firstCall.args[0] as CustomEvent<{ + message: string; + urgency: string; + }>; + expect(event.detail.message).to.equal('Row 2 deactivated'); + expect(event.detail.urgency).to.equal('polite'); + + document.body.removeEventListener('sl-announce', announceSpy); + }); }); describe('bulk actions', () => { diff --git a/packages/components/grid/src/grid.ts b/packages/components/grid/src/grid.ts index 89498877b3..c22adfcbb0 100644 --- a/packages/components/grid/src/grid.ts +++ b/packages/components/grid/src/grid.ts @@ -10,6 +10,7 @@ import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js'; +import { announce } from '@sl-design-system/announcer'; import { Button } from '@sl-design-system/button'; import { ArrayListDataSource, @@ -234,6 +235,9 @@ export class Grid extends ScopedElementsMixin(LitElement) { /** The virtualizer instance for the grid. */ #virtualizer?: VirtualizerHostElement[typeof virtualizerRef]; + /** Flag to skip the next focus announcement (e.g. after a click that already announced). */ + #skipNextFocusAnnounce = false; + /** The current active row. */ @property({ attribute: false }) activeRow?: T; @@ -406,6 +410,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { this.#mutationObserver?.observe(this.tbody, { attributes: true, attributeFilter: ['style'] }); this.tbody.addEventListener('scroll', () => this.#onScroll(), { passive: true }); + this.tbody.addEventListener('focusin', (event: FocusEvent) => this.#onFocusIn(event)); // Workaround for https://github.com/lit/lit/issues/4232 await new Promise(resolve => requestAnimationFrame(resolve)); @@ -594,25 +599,37 @@ export class Grid extends ScopedElementsMixin(LitElement) { renderItemRow(item: ListDataSourceDataItem, index: number): TemplateResult { const rows = this.#headerRows, + active = this.activeRow === item.data, selected = this.dataSource?.isSelected(item), parts = [ 'row', index % 2 === 0 ? 'odd' : 'even', ...(selected ? ['selected'] : []), - ...(this.activeRow === item.data ? ['active'] : []), + ...(active ? ['active'] : []), ...(this.#dragItem === item ? ['dragging'] : []), ...(this.itemParts?.(item.data)?.split(' ') || []) - ]; + ], + ariaCurrent = + this.rowAction === 'activate' + ? active + ? 'true' + : nothing + : (this.dataSource?.selects ?? this.selects) === 'single' + ? selected + ? 'true' + : nothing + : nothing; return html` this.#onClickRow(item)} + @click=${() => this.#onClickRow(item, index + 1)} @dragstart=${(event: DragEvent) => this.#onDragStart(event, item)} @dragenter=${(event: DragEvent) => this.#onDragEnter(event, item)} @dragover=${(event: DragEvent) => this.#onDragOver(event, item)} @dragend=${(event: DragEvent) => this.#onDragEnd(event, item)} @drop=${(event: DragEvent) => this.#onDrop(event, item)} aria-rowindex=${index + 1} + aria-current=${ariaCurrent} index=${index} part=${parts.join(' ')}> ${rows[rows.length - 1].map(col => col.renderData(item))} @@ -720,7 +737,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { this.dataSource?.update(); } - #onClickRow(item: ListDataSourceDataItem): void { + #onClickRow(item: ListDataSourceDataItem, index: number): void { if (this.rowAction === 'activate') { this.dataSource?.deselectAll(); this.dataSource?.update(); @@ -735,13 +752,64 @@ export class Grid extends ScopedElementsMixin(LitElement) { } else if (this.rowAction === 'select') { this.dataSource?.toggle(item); this.dataSource?.update(); + } else { + return; } + + this.#announceSelection(item, index); + this.#skipNextFocusAnnounce = true; + + // Reset the flag soon so it only skips focus events from this click (e.g. focus moving to a button in the row) + setTimeout(() => (this.#skipNextFocusAnnounce = false)); } #onColumnUpdate(event: Event & { target: GridColumn }): void { this.#addScopedElements(event.target.scopedElements); } + #announceSelection(item: ListDataSourceDataItem, index: number): void { + const selected = + this.rowAction === 'activate' ? !!this.activeRow : !!this.dataSource?.isSelected(item); + + // Add 1 to account for the header row + const rowNumber = index + 1; + + announce( + selected + ? msg(str`Row ${rowNumber} activated`, { id: 'sl.grid.rowActivated' }) + : msg(str`Row ${rowNumber} deactivated`, { id: 'sl.grid.rowDeactivated' }), + 'polite' + ); + } + + #onFocusIn(event: FocusEvent): void { + // Skip the focus announcement if it was triggered by a click that is already announced + if (this.#skipNextFocusAnnounce) { + this.#skipNextFocusAnnounce = false; + return; + } + + const row = (event.target as HTMLElement)?.closest?.('tr'); + + if (!row || this.rowAction !== 'activate' || !row.part.contains('active')) { + return; + } + + const index = row.getAttribute('aria-rowindex'); + + if (index) { + // Add 1 to account for the header row + const rowNumber = Number(index) + 1; + + // Use 'assertive' so the user knows right away which row they are in + announce( + msg(str`In activated row ${rowNumber}`, { id: 'sl.grid.inActivatedRow' }), + 'assertive', + true + ); + } + } + #onDataSourceUpdate = () => { this.requestUpdate(); }; diff --git a/packages/components/grid/src/stories/selection.stories.ts b/packages/components/grid/src/stories/selection.stories.ts index e02c6c0556..3a82b45605 100644 --- a/packages/components/grid/src/stories/selection.stories.ts +++ b/packages/components/grid/src/stories/selection.stories.ts @@ -36,7 +36,10 @@ Icon.register(faCopy, faRightToLine, faTrash); export const Activate: Story = { render: (_, { loaded: { students } }) => { + let activeStudent: Student | undefined; + const onActiveRowChange = ({ detail: student }: SlActiveRowChangeEvent): void => { + activeStudent = student; document.getElementById('selection')!.innerText = student ? `You have activated ${student.fullName}.` : 'You have not activated anybody yet.'; @@ -51,6 +54,15 @@ export const Activate: Story = { sl-grid-active-row-change event is dispatched when the active row changes, which you can use to update the UI or perform other actions based on the active row.

+

+ Note: For accessibility, you should add aria-pressed and + aria-description to the button that activates the row. Set + aria-pressed to "true" when the row is active and + "false" when it is not. Set aria-description to "Activate row" or + "Deactivate row" depending on the state. The grid does not set these attributes for you, so + you need to track the active row using the sl-grid-active-row-change event and + update your renderer accordingly. See the code below for an example. +

You have not activated anybody yet.

html` - ${avatarRenderer(student)} - `} + .renderer=${(student: Student) => { + const isActive = activeStudent === student; + + return html` + + ${avatarRenderer(student)} + + `; + }} .scopedElements=${{ 'sl-avatar': Avatar, 'sl-button': Button }}> @@ -446,7 +468,10 @@ export const WithFiltering: Story = { export const WithLinks: Story = { render: (_, { loaded: { students } }) => { + let activeStudent: Student | undefined; + const onActiveRowChange = ({ detail: student }: SlActiveRowChangeEvent): void => { + activeStudent = student; document.getElementById('selection')!.innerText = student ? `You have activated ${student.fullName}.` : 'You have not activated or selected anybody yet.'; @@ -458,6 +483,7 @@ export const WithLinks: Story = { if (selected > 0) { grid.activeRow = undefined; // Reset selected student when selection changes + activeStudent = undefined; selection.innerText = `You have selected ${selected} ${selected > 1 ? 'students' : 'student'}.`; } else { @@ -471,6 +497,15 @@ export const WithLinks: Story = { checkbox in the selection column while at the same time clicking anywhere else to activate the row. Using keyboard, you can do both.

+

+ Note: For accessibility, you should add aria-pressed and + aria-description to the button that activates the row. Set + aria-pressed to "true" when the row is active and + "false" when it is not. Set aria-description to "Activate row" or + "Deactivate row" depending on the state. The grid does not set these attributes for you, so + you need to track the active row using the sl-grid-active-row-change event and + update your renderer accordingly. See the code below for an example. +

You have not activated or selected anybody yet.

html` - ${avatarRenderer(student)} - `} + .renderer=${(student: Student) => { + const isActive = activeStudent === student; + + return html` + + ${avatarRenderer(student)} + + `; + }} .scopedElements=${{ 'sl-avatar': Avatar, 'sl-button': Button }}> diff --git a/packages/locales/src/es-ES.ts b/packages/locales/src/es-ES.ts index 27755fdbca..9681959f6f 100644 --- a/packages/locales/src/es-ES.ts +++ b/packages/locales/src/es-ES.ts @@ -55,8 +55,11 @@ export const templates = { 'sl.grid.blankFilterOption': 'Vacío', 'sl.grid.cancelSelection': 'Cancelar selección', 'sl.grid.filterByValue': str`Filtrar por ${0}`, + 'sl.grid.inActivatedRow': str`En la fila activada ${0}`, 'sl.grid.removeSort': 'Quitar orden', 'sl.grid.reorder': 'Reordenar', + 'sl.grid.rowActivated': str`Fila ${0} activada`, + 'sl.grid.rowDeactivated': str`Fila ${0} desactivada`, 'sl.grid.selectAllRows': 'Seleccionar todas las filas', 'sl.grid.selectionStatusMessage': str`${0} de ${1} seleccionados`, 'sl.grid.selectRow': 'Seleccionar fila', diff --git a/packages/locales/src/es-ES.xlf b/packages/locales/src/es-ES.xlf index 44c18375ef..e16b202cdc 100644 --- a/packages/locales/src/es-ES.xlf +++ b/packages/locales/src/es-ES.xlf @@ -462,6 +462,18 @@ More information Más información + + Row activated + Fila activada + + + Row deactivated + Fila desactivada + + + In activated row + En la fila activada + diff --git a/packages/locales/src/it.ts b/packages/locales/src/it.ts index d024ad7582..4743dc1b4b 100644 --- a/packages/locales/src/it.ts +++ b/packages/locales/src/it.ts @@ -55,8 +55,11 @@ export const templates = { 'sl.grid.blankFilterOption': 'Vuoto', 'sl.grid.cancelSelection': 'Annulla selezione', 'sl.grid.filterByValue': str`Filtra per ${0}`, + 'sl.grid.inActivatedRow': str`Nella riga attivata ${0}`, 'sl.grid.removeSort': 'Rimuovi ordinamento', 'sl.grid.reorder': 'Riordinare', + 'sl.grid.rowActivated': str`Riga ${0} attivata`, + 'sl.grid.rowDeactivated': str`Riga ${0} disattivata`, 'sl.grid.selectAllRows': 'Seleziona tutte le righe', 'sl.grid.selectionStatusMessage': str`${0} di ${1} selezionati`, 'sl.grid.selectRow': 'Seleziona riga', diff --git a/packages/locales/src/it.xlf b/packages/locales/src/it.xlf index aba8f89b36..52babb3aa7 100644 --- a/packages/locales/src/it.xlf +++ b/packages/locales/src/it.xlf @@ -462,6 +462,18 @@ More information Più informazioni + + Row activated + Riga attivata + + + Row deactivated + Riga disattivata + + + In activated row + Nella riga attivata + diff --git a/packages/locales/src/nl.ts b/packages/locales/src/nl.ts index c9c241eb64..bfd17adfae 100644 --- a/packages/locales/src/nl.ts +++ b/packages/locales/src/nl.ts @@ -55,8 +55,11 @@ export const templates = { 'sl.grid.blankFilterOption': 'Leeg', 'sl.grid.cancelSelection': 'Annuleer selectie', 'sl.grid.filterByValue': str`Filter op ${0}`, + 'sl.grid.inActivatedRow': str`In geactiveerde rij ${0}`, 'sl.grid.removeSort': 'Verwijder sortering', 'sl.grid.reorder': 'Herschikken', + 'sl.grid.rowActivated': str`Rij ${0} geactiveerd`, + 'sl.grid.rowDeactivated': str`Rij ${0} gedeactiveerd`, 'sl.grid.selectAllRows': 'Selecteer alle rijen', 'sl.grid.selectionStatusMessage': str`${0} van ${1} geselecteerd`, 'sl.grid.selectRow': 'Selecteer rij', diff --git a/packages/locales/src/nl.xlf b/packages/locales/src/nl.xlf index 4f0c394971..ec255fb5fd 100644 --- a/packages/locales/src/nl.xlf +++ b/packages/locales/src/nl.xlf @@ -462,6 +462,18 @@ Options Opties + + Row activated + Rij geactiveerd + + + Row deactivated + Rij gedeactiveerd + + + In activated row + In geactiveerde rij + diff --git a/packages/locales/src/pl.ts b/packages/locales/src/pl.ts index 462fc2b037..ed9be06347 100644 --- a/packages/locales/src/pl.ts +++ b/packages/locales/src/pl.ts @@ -55,8 +55,11 @@ export const templates = { 'sl.grid.blankFilterOption': 'Puste', 'sl.grid.cancelSelection': 'Anuluj wybór', 'sl.grid.filterByValue': str`Filtruj według ${0}`, + 'sl.grid.inActivatedRow': str`W aktywowanym wierszu ${0}`, 'sl.grid.removeSort': 'Usuń sortowanie', 'sl.grid.reorder': 'Zmień kolejność', + 'sl.grid.rowActivated': str`Wiersz ${0} aktywowany`, + 'sl.grid.rowDeactivated': str`Wiersz ${0} dezaktywowany`, 'sl.grid.selectAllRows': 'Zaznacz wszystkie wiersze', 'sl.grid.selectionStatusMessage': str`${0} z ${1} wybranych`, 'sl.grid.selectRow': 'Zaznacz wiersz', diff --git a/packages/locales/src/pl.xlf b/packages/locales/src/pl.xlf index 7b71b2c524..788b1b0b17 100644 --- a/packages/locales/src/pl.xlf +++ b/packages/locales/src/pl.xlf @@ -462,6 +462,18 @@ More information Więcej informacji + + Row activated + Wiersz aktywowany + + + Row deactivated + Wiersz dezaktywowany + + + In activated row + W aktywowanym wierszu + diff --git a/yarn.lock b/yarn.lock index 2817c63acd..b3e7028e10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5922,6 +5922,7 @@ __metadata: "@lit-labs/virtualizer": "npm:^2.1.1" "@lit/localize": "npm:^0.12.2" "@open-wc/scoped-elements": "npm:^3.0.6" + "@sl-design-system/announcer": "npm:^0.0.8" "@sl-design-system/button": "npm:^2.1.0" "@sl-design-system/checkbox": "npm:^2.1.10" "@sl-design-system/data-source": "npm:^0.4.0"