diff --git a/kolibri/plugins/safe_html5_viewer/assets/__tests__/SafeHtml5RendererIndex.spec.js b/kolibri/plugins/safe_html5_viewer/assets/__tests__/SafeHtml5RendererIndex.spec.js index 32722888095..6859dee4683 100644 --- a/kolibri/plugins/safe_html5_viewer/assets/__tests__/SafeHtml5RendererIndex.spec.js +++ b/kolibri/plugins/safe_html5_viewer/assets/__tests__/SafeHtml5RendererIndex.spec.js @@ -92,34 +92,6 @@ describe('SafeHtml5RendererIndex', () => { }); }); - describe('windowSizeClass pass-through', () => { - test("the 'small-window' class is applied to table caption when innerWidth <= 600", async () => { - Object.defineProperty(window, 'innerWidth', { - writable: true, - configurable: true, - value: 500, - }); - renderComponent(); - await waitFor(() => { - const caption = screen.getByText('Mocked 3-column Table'); - expect(caption).toHaveClass('small-window'); - }); - }); - - test("the 'small-window' class is not applied to table caption when innerWidth > 600", async () => { - Object.defineProperty(window, 'innerWidth', { - writable: true, - configurable: true, - value: 800, - }); - renderComponent(); - await waitFor(() => { - const caption = screen.getByText('Mocked 3-column Table'); - expect(caption).not.toHaveClass('small-window'); - }); - }); - }); - describe('progress tracking', () => { test('emits startTracking on created', async () => { const { emitted } = renderComponent(); diff --git a/kolibri/plugins/safe_html5_viewer/assets/src/views/SafeHtml5RendererIndex.vue b/kolibri/plugins/safe_html5_viewer/assets/src/views/SafeHtml5RendererIndex.vue index 086147b77b2..abfd1be5a75 100644 --- a/kolibri/plugins/safe_html5_viewer/assets/src/views/SafeHtml5RendererIndex.vue +++ b/kolibri/plugins/safe_html5_viewer/assets/src/views/SafeHtml5RendererIndex.vue @@ -15,12 +15,7 @@ role="region" :aria-label="$tr('articleContent')" > - + @@ -32,8 +27,6 @@ import ZipFile from 'kolibri-zip'; import SafeHTML from 'kolibri-common/components/SafeHTML'; import useContentViewer, { contentViewerProps } from 'kolibri/composables/useContentViewer'; - import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; - import { computed } from 'vue'; export default { name: 'SafeHtml5RendererIndex', @@ -41,17 +34,12 @@ SafeHTML, }, setup(props, context) { - const { windowIsSmall } = useKResponsiveWindow(); - const windowSizeClass = computed(() => { - return windowIsSmall.value ? 'small-window' : ''; - }); const { defaultFile, forceDurationBasedProgress, durationBasedProgress } = useContentViewer( props, context, { defaultDuration: 300 }, ); return { - windowSizeClass, defaultFile, forceDurationBasedProgress, durationBasedProgress, diff --git a/packages/kolibri-common/components/SafeHTML/SafeHtmlTable.js b/packages/kolibri-common/components/SafeHTML/SafeHtmlTable.js deleted file mode 100644 index 3ef2d9fc074..00000000000 --- a/packages/kolibri-common/components/SafeHTML/SafeHtmlTable.js +++ /dev/null @@ -1,54 +0,0 @@ -export default { - name: 'SafeHtmlTable', - functional: true, - props: { - node: { required: true }, - attributes: { type: Object, required: true }, - tableCounter: { type: Number, required: true }, - windowSizeClass: { type: String, default: '' }, - mapNode: { type: Function, required: true }, - mapChildren: { type: Function, required: true }, - }, - render(h, context) { - const { node, attributes, tableCounter, windowSizeClass, mapNode, mapChildren } = context.props; - const captionId = `table-caption-${tableCounter}`; - - const children = []; - for (const childNode of node.childNodes) { - if ( - childNode.nodeType === Node.ELEMENT_NODE && - childNode.tagName.toLowerCase() === 'caption' - ) { - const captionAttrs = {}; - for (const attr of childNode.attributes) { - captionAttrs[attr.name] = attr.value; - } - captionAttrs.id = captionId; - captionAttrs.class = windowSizeClass ? `safe-html ${windowSizeClass}` : 'safe-html'; - children.push(h('caption', { attrs: captionAttrs }, mapChildren(childNode.childNodes))); - } else { - children.push(mapNode(childNode)); - } - } - - const firstRow = node.querySelector && node.querySelector('tr'); - const colCount = firstRow ? firstRow.children.length : 0; - let tableWidth = '640px'; - if (colCount > 3) { - tableWidth = `${colCount * 200}px`; - } - - return h( - 'div', - { - class: 'table-container', - attrs: { - role: 'region', - 'aria-labelledby': captionId, - 'data-testid': 'table-container', - }, - }, - [h('table', { attrs: attributes, style: { width: tableWidth } }, children)], - ); - }, -}; diff --git a/packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue b/packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue new file mode 100644 index 00000000000..4595f448498 --- /dev/null +++ b/packages/kolibri-common/components/SafeHTML/SafeHtmlTable.vue @@ -0,0 +1,135 @@ + + + + + + + diff --git a/packages/kolibri-common/components/SafeHTML/__tests__/SafeHtmlTable.spec.js b/packages/kolibri-common/components/SafeHTML/__tests__/SafeHtmlTable.spec.js index 66f149d0611..7fd274b8eb3 100644 --- a/packages/kolibri-common/components/SafeHTML/__tests__/SafeHtmlTable.spec.js +++ b/packages/kolibri-common/components/SafeHTML/__tests__/SafeHtmlTable.spec.js @@ -1,12 +1,10 @@ import { render, screen } from '@testing-library/vue'; -import { h } from 'vue'; -import SafeHtmlTable from '../SafeHtmlTable.js'; +import SafeHtmlTable from '../SafeHtmlTable.vue'; // Create a table element with m rows and n columns const createSampleNode = (m, n) => { const table = document.createElement('table'); - // Caption const caption = document.createElement('caption'); caption.textContent = 'Sample Caption'; table.appendChild(caption); @@ -15,7 +13,6 @@ const createSampleNode = (m, n) => { return table; } - // Thead (row 1) const thead = document.createElement('thead'); const theadRow = document.createElement('tr'); for (let col = 1; col <= n; col++) { @@ -57,27 +54,14 @@ const createSampleNode = (m, n) => { }; const sampleAttributes = { class: 'safe-html' }; -const sampleTableCounter = 6; -const sampleWindowSizeClass = 'small-window'; - -const mapNode = jest.fn(node => { - if (node.nodeType === Node.ELEMENT_NODE) { - return h(node.tagName.toLowerCase(), { attrs: sampleAttributes }, mapChildren(node.childNodes)); - } else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '') { - return node.textContent; - } - return null; -}); - -const mapChildren = jest.fn(childNodes => Array.from(childNodes).map(mapNode).filter(Boolean)); +const mapNode = jest.fn(() => null); +const mapChildren = jest.fn(() => []); const renderComponent = (m, n) => { return render(SafeHtmlTable, { props: { node: createSampleNode(m, n), attributes: sampleAttributes, - tableCounter: sampleTableCounter, - windowSizeClass: sampleWindowSizeClass, mapNode, mapChildren, }, @@ -98,46 +82,10 @@ describe('SafeHtmlTable', () => { expect(screen.getByRole('table')).toBeInTheDocument(); }); - test('renders the caption', () => { - expect(screen.getByText('Sample Caption')).toBeInTheDocument(); - }); - }); - - describe('class and a11y attributes', () => { - beforeEach(() => { - renderComponent(3, 3); - }); - - test('table has safe-html class', () => { - expect(screen.getByRole('table')).toHaveClass('safe-html'); - }); - - test('caption has safe-html and small-window classes', () => { - expect(screen.getByText('Sample Caption')).toHaveClass('safe-html', 'small-window'); - }); - - test('non-caption child nodes have safe-html class', () => { - const rowGroups = screen.getAllByRole('rowgroup'); // thead, tbody, tfoot - rowGroups.forEach(rowGroup => expect(rowGroup).toHaveClass('safe-html')); - - const trs = screen.getAllByRole('row'); - trs.forEach(tr => expect(tr).toHaveClass('safe-html')); - - const ths = screen.getAllByRole('columnheader'); - ths.forEach(th => expect(th).toHaveClass('safe-html')); - const tds = screen.getAllByRole('cell'); - tds.forEach(td => expect(td).toHaveClass('safe-html')); - }); - - test('caption has correct id', () => { - expect(screen.getByText('Sample Caption')).toHaveAttribute('id', 'table-caption-6'); - }); - - test('container div has correct aria-labelledby', () => { - expect(screen.getByTestId('table-container')).toHaveAttribute( - 'aria-labelledby', - 'table-caption-6', - ); + test('renders a caption element', () => { + const table = screen.getByRole('table'); + const caption = table.querySelector('caption'); + expect(caption).not.toBeNull(); }); }); diff --git a/packages/kolibri-common/components/SafeHTML/index.js b/packages/kolibri-common/components/SafeHTML/index.js index 797fb5769ff..ed1f2c808ec 100644 --- a/packages/kolibri-common/components/SafeHTML/index.js +++ b/packages/kolibri-common/components/SafeHTML/index.js @@ -1,7 +1,7 @@ import DOMPurify from 'dompurify'; import kebabCase from 'lodash/kebabCase'; import './style.scss'; -import SafeHtmlTable from './SafeHtmlTable.js'; +import SafeHtmlTable from './SafeHtmlTable.vue'; import SafeHtmlImage from './SafeHtmlImage.vue'; const ALLOWED_URI_REGEXP = /^(?:(?:blob:https?|data):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i; @@ -46,7 +46,7 @@ export function createSafeHTML(customComponents = {}) { }, RETURN_DOM_FRAGMENT: true, }); - let tableCounter = 0; + function mapNode(node) { if (node.nodeType === Node.ELEMENT_NODE) { const tagName = node.tagName.toLowerCase(); @@ -88,17 +88,11 @@ export function createSafeHTML(customComponents = {}) { attributes[attr.name] = attr.value; } attributes.class = attributes.class ? `${attributes.class} safe-html` : 'safe-html'; - if (tagName === 'table') { - tableCounter += 1; return h(SafeHtmlTable, { props: { node, attributes, - tableCounter, - windowSizeClass: context.props.styleOverrides - ? context.props.styleOverrides.windowSizeClass - : '', mapNode, mapChildren, },