diff --git a/example/TableApp/package.json b/example/TableApp/package.json index dc49ce9d..31815780 100644 --- a/example/TableApp/package.json +++ b/example/TableApp/package.json @@ -10,7 +10,7 @@ "lint": "eslint ." }, "dependencies": { - "@nebula.js/sn-table": "file:../../prebuilt/nebula.js-react-native-sn-table-1.14.121.tgz", + "@nebula.js/sn-table": "file:../../prebuilt/react-native-sn-table", "@nebula.js/stardust": "2.9.0", "@qlik/carbon-core": "2.0.4", "@qlik/react-native-carbon": "2.1.18", diff --git a/example/TableApp/yarn.lock b/example/TableApp/yarn.lock index cd9bd474..b803811b 100644 --- a/example/TableApp/yarn.lock +++ b/example/TableApp/yarn.lock @@ -1179,9 +1179,8 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@nebula.js/sn-table@file:../../prebuilt/nebula.js-react-native-sn-table-1.14.121.tgz": +"@nebula.js/sn-table@file:../../prebuilt/package": version "1.14.121" - resolved "file:../../prebuilt/nebula.js-react-native-sn-table-1.14.121.tgz#732730d20345059416bfec28fa3a6790b53cbeee" "@nebula.js/stardust@2.9.0": version "2.9.0" @@ -1206,7 +1205,8 @@ integrity sha512-F2ooCVg9Yv99xATZCePA3WdQCNd1jlvxpWb83GzzF/EOmDyvdV6KkX9gm1aC2jiR386bsyZ20akB4kA6Hdo6pA== "@qlik/react-native-simple-grid@link:../..": - version "1.2.12" + version "0.0.0" + uid "" "@react-native-async-storage/async-storage@1.17.6": version "1.17.6" diff --git a/prebuilt/nebula.js-react-native-sn-table-1.14.121.tgz b/prebuilt/nebula.js-react-native-sn-table-1.14.121.tgz deleted file mode 100644 index 07df48d5..00000000 Binary files a/prebuilt/nebula.js-react-native-sn-table-1.14.121.tgz and /dev/null differ diff --git a/prebuilt/react-native-sn-table/conditional-colors.ts b/prebuilt/react-native-sn-table/conditional-colors.ts new file mode 100644 index 00000000..38450d44 --- /dev/null +++ b/prebuilt/react-native-sn-table/conditional-colors.ts @@ -0,0 +1,217 @@ +/* eslint-disable prefer-destructuring */ +import { TableCell } from './types'; + +export type PaletteColor = { + color: string; + icon: string; + index: number; +}; + +export type Limit = { + value: number; + gradient: boolean; + normal: number; +}; + +export type Segments = { + limits: Array; + paletteColors: Array; +}; + +export type ConditionalColoring = { + segments: Segments; +}; + +export type Indicator = { + applySegmentColors: boolean; + position: string; + showTextValues: boolean; +}; + +export type Respresentaton = { + indicator: Indicator; +}; + +export type Column = { + conditionalColoring: ConditionalColoring; + representation: Respresentaton; + qMin?: number; + qMax?: number; +}; + +export type LimitBound = { + upperIndex: number; + lowerIndex: number; +}; + +export type ConditionalColoringMeasureInfo = EngineAPI.INxMeasureInfo & { conditionalColoring?: ConditionalColoring }; + +function interpolate(a: number, b: number, amount: number) { + return a + (b - a) * amount; +} + +export function lerp(a: string, b: string, amount: number) { + if (!a.startsWith('#') || !b.startsWith('#')) { + return 'none'; + } + const from = parseInt(a.slice(1, 7), 16); + const to = parseInt(b.slice(1, 7), 16); + const ar = (from & 0xff0000) >> 16; + const ag = (from & 0x00ff00) >> 8; + const ab = from & 0x0000ff; + const br = (to & 0xff0000) >> 16; + const bg = (to & 0x00ff00) >> 8; + const bb = to & 0x0000ff; + const rr = interpolate(ar, br, amount); + const rg = interpolate(ag, bg, amount); + const rb = interpolate(ab, bb, amount); + + // 6 because it's a hex + return `#${((rr << 16) + (rg << 8) + (rb | 0)).toString(16).padStart(6, '0').slice(-6)}`; +} + +function getAutoMinMax(lmts: Array, val: number) { + let limits = lmts.map((l: Limit) => l.value); + + let range; + let delta = 0; + + limits = limits.sort((a, b) => a - b); + const nanLimits = limits.some((l) => Number.isNaN(+l)); + + // eslint-disable-next-line no-nested-ternary + const value = !Number.isNaN(+val) ? val : !Number.isNaN(+limits[0]) ? limits[0] : 0.5; + + if (limits.length === 0 || nanLimits) { + if (value === 0) { + delta = 0; + } else { + delta = value * 0.5; + } + } else if (limits.length === 1) { + range = limits[0] - value; + + if (range === 0 || Number.isNaN(+range)) { + if (value === 0) { + delta = 0.5; + } else { + delta = Math.abs(value * 0.5); + } + } else if (range > 0) { + // limit on right side of value + return { + min: value - Math.abs(value) * 0.5, + max: limits[0] + 2 * range, + }; + } else { + return { + min: limits[0] + 2 * range, + max: value + Math.abs(value) * 0.5, + }; + } + } else { + // 2 or more limits + range = limits[limits.length - 1] - limits[0]; + range = range > 0 ? range : 1; + return { + min: Math.min(value, limits[0]) - range / 2, + max: Math.max(value, limits[limits.length - 1]) + range / 2, + }; + } + + return { + min: value - delta, + max: value + delta, + }; +} + +function shouldBlend(segmentInfo: Segments, colorIndex: number) { + if (segmentInfo.limits.length === 0) { + return false; + } + + let returnBoolean; + + if (colorIndex === 0) { + // first color + returnBoolean = segmentInfo.limits[colorIndex].gradient; + } else if (colorIndex === segmentInfo.limits.length) { + returnBoolean = segmentInfo.limits[colorIndex - 1].gradient; + } else { + returnBoolean = segmentInfo.limits[colorIndex].gradient || segmentInfo.limits[colorIndex - 1].gradient; + } + + return returnBoolean; +} + +export function getConditionalColor(paletteIndex: number, value: number, column: Column): PaletteColor { + if (!shouldBlend(column.conditionalColoring.segments, paletteIndex)) { + return { ...column.conditionalColoring.segments.paletteColors[paletteIndex], ...column.representation.indicator }; + } + + const { paletteColors } = column.conditionalColoring.segments; + const { limits } = column.conditionalColoring.segments; + const { min, max } = getAutoMinMax(limits, value); + const segmentLimits = limits.map((l) => { + const limitNormal = max === min ? 0.5 : (l.value - min) / (max - min); + return { ...l, normal: limitNormal }; + }); + const mag = max - min; + let t = 1.0; + let color = 'none'; + + const normal = (value - min) / mag; + const lowerLimit = paletteIndex > 0 ? segmentLimits[paletteIndex - 1] : { normal: 0, value: 0, gradient: false }; + const upperLimit = segmentLimits[paletteIndex] + ? segmentLimits[paletteIndex] + : { normal: 1, value: 0, gradient: false }; + const prevColor = paletteColors[paletteIndex - (paletteIndex > 0 ? 1 : 0)]; + const nextColor = paletteColors[paletteIndex + (paletteIndex < paletteColors.length - 1 ? 1 : 0)]; + const normValueInLimits = (normal - lowerLimit.normal) / (upperLimit.normal - lowerLimit.normal); + if (lowerLimit && lowerLimit.gradient && upperLimit && upperLimit.gradient) { + // triple gradient + if (normal - lowerLimit.value < upperLimit.value - normal) { + // closer to the lower limit + color = prevColor.color; + t = 0.5 - normValueInLimits; + } else { + color = nextColor.color; + t = normValueInLimits - 0.5; + } + } else if (lowerLimit && lowerLimit.gradient) { + color = prevColor.color; + t = 1 - (0.5 + normValueInLimits / 2); + } else if (upperLimit && upperLimit.gradient) { + color = nextColor.color; + t = normValueInLimits / 2; + } else { + t = normal; + } + + return { + ...column.representation.indicator, + ...paletteColors[paletteIndex], + color: lerp(paletteColors[paletteIndex].color, color, t), + }; +} + +export function getIndicator(column: Column, tableCell: TableCell): PaletteColor | undefined { + if ( + column.conditionalColoring && + tableCell.qNum !== undefined && + column.conditionalColoring.segments.limits.length > 0 + ) { + let index = 0; + const cl = column.conditionalColoring.segments.limits.length; + + while (index < cl && tableCell.qNum > column.conditionalColoring.segments.limits[index].value) { + index++; + } + const idc = getConditionalColor(index, tableCell.qNum, column); + return idc; + } + if (column.conditionalColoring && tableCell.qNum && column.conditionalColoring.segments.paletteColors.length === 1) { + return { ...column.conditionalColoring.segments.paletteColors[0], ...column.representation.indicator }; + } + return undefined; +} diff --git a/prebuilt/react-native-sn-table/data.js b/prebuilt/react-native-sn-table/data.js new file mode 100644 index 00000000..76f42bb1 --- /dev/null +++ b/prebuilt/react-native-sn-table/data.js @@ -0,0 +1,15 @@ +export default () => ({ + targets: [ + { + path: '/qHyperCubeDef', + dimensions: { + min: 0, + max: 1000, + }, + measures: { + min: 0, + max: 1000, + }, + }, + ], +}); diff --git a/prebuilt/react-native-sn-table/ext.js b/prebuilt/react-native-sn-table/ext.js new file mode 100644 index 00000000..93829465 --- /dev/null +++ b/prebuilt/react-native-sn-table/ext.js @@ -0,0 +1,529 @@ +import Modifiers from 'qlik-modifiers'; + +const columnCommonHidden = { + autoSort: { + ref: 'qDef.autoSort', + type: 'boolean', + defaultValue: true, + show: false, + }, +}; + +const columnExpressionItems = { + visibilityCondition: { + type: 'string', + component: 'expression', + ref: 'qCalcCondition.qCond', + expression: 'optional', + expressionType: 'ValueExpr', + translation: 'Object.Table.Columns.VisibilityCondition', + defaultValue: { qv: '' }, + tid: 'visibilityCondition', + isExpression: (val) => typeof val === 'string' && val.trim().length > 0, + }, + tableCellColoring: { + component: 'attribute-expression-reference', + defaultValue: [], + ref: 'qAttributeExpressions', + items: [ + { + component: 'expression', + ref: 'qExpression', + expressionType: 'measure', + translation: 'Object.Table.Measure.BackgroundExpression', + defaultValue: '', + id: 'cellBackgroundColor', + tid: 'tableColorBgByExpression', + }, + { + component: 'expression', + ref: 'qExpression', + expressionType: 'measure', + translation: 'Object.Table.Measure.ForegroundExpression', + defaultValue: '', + id: 'cellForegroundColor', + tid: 'tableColorByExpression', + }, + ], + }, +}; + +const textAlignItems = { + textAlignAuto: { + ref: 'qDef.textAlign.auto', + type: 'boolean', + component: 'switch', + translation: 'Common.Text.TextAlignment', + options: [ + { + value: true, + translation: 'Common.Auto', + }, + { + value: false, + translation: 'Common.Custom', + }, + ], + defaultValue: true, + }, + textAlign: { + ref: 'qDef.textAlign.align', + type: 'string', + component: 'item-selection-list', + horizontal: true, + items: [ + { + component: 'icon-item', + icon: 'align_left', + labelPlacement: 'bottom', + value: 'left', + translation: 'properties.dock.left', + }, + { + component: 'icon-item', + icon: 'align_center', + labelPlacement: 'bottom', + value: 'center', + translation: 'Common.Center', + }, + { + component: 'icon-item', + icon: 'align_right', + labelPlacement: 'bottom', + value: 'right', + translation: 'properties.dock.right', + }, + ], + defaultValue: 'left', + show: (data) => data.qDef.textAlign !== undefined && !data.qDef.textAlign.auto, + }, +}; + +const getStyleSettings = (env) => { + return [ + { + type: 'items', + items: [ + { + component: 'style-editor', + translation: 'LayerStyleEditor.component.styling', + subtitle: 'LayerStyleEditor.component.styling', + resetBtnTranslation: 'LayerStyleEditor.component.resetAll', + key: 'theme', + ref: 'components', + defaultValue: [], // used by chart conversion + defaultValues: { + // used by style editor + key: 'theme', + content: { + fontSize: null, + fontColor: { + index: -1, + color: null, + }, + hoverEffect: false, + hoverColor: { + index: -1, + color: null, + }, + hoverFontColor: { + index: -1, + color: null, + }, + }, + header: { + fontSize: null, + fontColor: { + index: -1, + color: null, + }, + }, + }, + items: { + chart: { + type: 'items', + items: { + headerFontSize: { + show: true, + ref: 'header.fontSize', + translation: 'ThemeStyleEditor.style.headerFontSize', + component: 'integer', + // placeholder: () => parseInt(styleService.getStyle('header', 'fontSize'), 10), + maxlength: 3, + change(data) { + data.header.fontSize = Math.max(5, Math.min(300, Math.floor(data.header.fontSize))); + }, + }, + headerFontColor: { + show: true, + ref: 'header.fontColor', + translation: 'ThemeStyleEditor.style.headerFontColor', + type: 'object', + component: 'color-picker', + dualOutput: true, + }, + fontSize: { + show: true, + translation: 'ThemeStyleEditor.style.cellFontSize', + ref: 'content.fontSize', + component: 'integer', + // placeholder: () => parseInt(styleService.getStyle('content', 'fontSize'), 10), + maxlength: 3, + change(data) { + data.content.fontSize = Math.max(5, Math.min(300, Math.floor(data.content.fontSize))); + }, + }, + fontColor: { + show: true, + ref: 'content.fontColor', + translation: 'ThemeStyleEditor.style.cellFontColor', + type: 'object', + component: 'color-picker', + dualOutput: true, + }, + hoverEffect: { + show: true, + ref: 'content.hoverEffect', + translation: 'ThemeStyleEditor.style.hoverEffect', + type: 'boolean', + component: 'switch', + options: [ + { + value: true, + translation: 'properties.on', + }, + { + value: false, + translation: 'properties.off', + }, + ], + }, + hoverColor: { + show: (data) => !!data.content.hoverEffect, + ref: 'content.hoverColor', + translation: 'ThemeStyleEditor.style.hoverStyle', + type: 'object', + component: 'color-picker', + dualOutput: true, + }, + hoverFontColor: { + show: (data) => !!data.content.hoverEffect, + ref: 'content.hoverFontColor', + translation: 'ThemeStyleEditor.style.hoverFontStyle', + type: 'object', + component: 'color-picker', + dualOutput: true, + }, + }, + }, + }, + }, + ], + }, + { + type: 'items', + items: [ + { + ref: 'totals.show', + type: 'boolean', + translation: 'properties.totals', + component: 'switch', + options: [ + { + value: true, + translation: 'Common.Auto', + }, + { + value: false, + translation: 'Common.Custom', + }, + ], + defaultValue: true, + }, + { + ref: 'totals.position', + translation: 'Common.Position', + type: 'string', + component: 'dropdown', + options: [ + { + value: 'noTotals', + translation: 'Common.None', + }, + { + value: 'top', + translation: 'Common.Top', + }, + { + value: 'bottom', + translation: 'Common.Bottom', + }, + ], + defaultValue: 'noTotals', + show(data) { + return !data.totals.show; + }, + }, + { + ref: 'totals.label', + translation: 'properties.totals.label', + type: 'string', + expression: 'optional', + defaultValue() { + return env.translator.get('Object.Table.Totals'); + }, + }, + ], + show: !env?.anything?.sense?.isUnsupportedFeature?.('totals'), + }, + ]; +}; + +const getDefinition = (env) => { + return { + type: 'items', + component: 'accordion', + items: { + data: { + type: 'items', + component: 'columns', + translation: 'Common.Data', + sortIndexRef: 'qHyperCubeDef.qColumnOrder', + allowMove: true, + allowAdd: true, + addTranslation: 'Common.Columns', + items: { + dimensions: { + type: 'array', + component: 'expandable-items', + ref: 'qHyperCubeDef.qDimensions', + grouped: true, + items: { + libraryId: { + type: 'string', + component: 'library-item', + libraryItemType: 'dimension', + ref: 'qLibraryId', + translation: 'Common.Dimension', + show(itemData) { + return itemData.qLibraryId; + }, + }, + inlineDimension: { + component: 'inline-dimension', + show(itemData) { + return !itemData.qLibraryId; + }, + }, + nullSuppression: { + type: 'boolean', + ref: 'qNullSuppression', + defaultValue: false, + translation: 'properties.dimensions.showNull', + inverted: true, + }, + ...columnCommonHidden, + ...columnExpressionItems, + ...textAlignItems, + }, + }, + measures: { + type: 'array', + component: 'expandable-items', + ref: 'qHyperCubeDef.qMeasures', + grouped: true, + items: { + libraryId: { + type: 'string', + component: 'library-item', + libraryItemType: 'measure', + ref: 'qLibraryId', + translation: 'Common.Measure', + show: (itemData) => itemData.qLibraryId, + }, + inlineMeasure: { + component: 'inline-measure', + show: (itemData) => !itemData.qLibraryId, + }, + ...columnCommonHidden, + ...columnExpressionItems, + ...textAlignItems, + totalsAggr: { + type: 'items', + grouped: true, + items: { + totalsAggrGroup: { + type: 'items', + items: { + totalsAggrFunc: { + type: 'string', + component: 'dropdown', + ref: 'qDef.qAggrFunc', + translation: 'Object.Table.AggrFunc', + options(data, handler) { + const hasActiveModifiers = Modifiers.hasActiveModifiers({ + measures: [data], + properties: handler.properties, + }); + const enableTotal = !hasActiveModifiers || Modifiers.ifEnableTotalsFunction(data); + const autoOption = enableTotal + ? [ + { + value: 'Expr', + translation: 'Common.Auto', + }, + ] + : []; + return autoOption.concat([ + { + value: 'Avg', + translation: 'Object.Table.AggrFunc.Avg', + }, + { + value: 'Count', + translation: 'Object.Table.AggrFunc.Count', + }, + { + value: 'Max', + translation: 'Object.Table.AggrFunc.Max', + }, + { + value: 'Min', + translation: 'Object.Table.AggrFunc.Min', + }, + { + value: 'Sum', + translation: 'Object.Table.AggrFunc.Sum', + }, + { + value: 'None', + translation: 'Object.Table.AggrFunc.None', + }, + ]); + }, + defaultValue: env?.anything?.sense?.isUnsupportedFeature?.('totals') ? 'None' : 'Expr', + }, + }, + }, + }, + show: !env?.anything?.sense?.isUnsupportedFeature?.('totals'), + }, + }, + }, + }, + }, + sorting: { + uses: 'sorting', + }, + addOns: { + type: 'items', + component: 'expandable-items', + translation: 'properties.addons', + items: { + dataHandling: { + uses: 'dataHandling', + items: { + calcCond: { + uses: 'calcCond', + }, + }, + }, + }, + }, + settings: { + uses: 'settings', + items: { + presentation: { + grouped: true, + type: 'items', + translation: 'properties.presentation', + items: getStyleSettings(env), + }, + }, + }, + }, + }; +}; + +export function indexAdded(array, index) { + let i; + for (i = 0; i < array.length; ++i) { + if (array[i] >= 0 && array[i] >= index) { + ++array[i]; + } + } + array.push(index); +} + +export function indexRemoved(array, index) { + let removeIndex = 0; + let i; + for (i = 0; i < array.length; ++i) { + if (array[i] > index) { + --array[i]; + } else if (array[i] === index) { + removeIndex = i; + } + } + array.splice(removeIndex, 1); + return removeIndex; +} + +export function min(nDimsOrMeas) { + return nDimsOrMeas > 0 ? 0 : 1; +} + +export function getDescription(env) { + return env.translator.get('Visualizations.Descriptions.Column'); +} + +export default function ext(env) { + return { + definition: getDefinition(env), + data: { + measures: { + min, + max: 1000, + description: () => getDescription(env), + add(measure, data, hcHandler) { + const { qColumnOrder, columnWidths } = hcHandler.hcProperties; + const ix = hcHandler.getDimensions().length + hcHandler.getMeasures().length - 1; + + indexAdded(qColumnOrder, ix); + + columnWidths.splice(qColumnOrder[ix], 0, -1); // -1 is auto + }, + remove(measure, data, hcHandler, idx) { + const { qColumnOrder, columnWidths } = hcHandler.hcProperties; + const columnIx = (hcHandler.hcProperties.qDimensions ? hcHandler.hcProperties.qDimensions.length : 0) + idx; + indexRemoved(qColumnOrder, columnIx); + columnWidths.splice(columnIx, 1); + }, + }, + dimensions: { + min, + max: 1000, + description: () => getDescription(env), + add(dimension, data, hcHandler) { + const { qColumnOrder, columnWidths } = hcHandler.hcProperties; + const ix = hcHandler.getDimensions().length - 1; + indexAdded(qColumnOrder, ix); + columnWidths.splice(ix, 0, -1); // -1 is auto + + return dimension; + }, + remove(dimension, data, hcHandler, idx) { + const { qColumnOrder, columnWidths } = hcHandler.hcProperties; + indexRemoved(qColumnOrder, idx); + columnWidths.splice(qColumnOrder[idx], 1); + }, + }, + }, + support: { + export: true, + exportData: true, + snapshot: true, + viewData: false, + }, + }; +} diff --git a/prebuilt/react-native-sn-table/handle-data.js b/prebuilt/react-native-sn-table/handle-data.js new file mode 100644 index 00000000..f17af61a --- /dev/null +++ b/prebuilt/react-native-sn-table/handle-data.js @@ -0,0 +1,241 @@ +import { getIndicator } from './conditional-colors'; +import { isDarkColor } from './table/utils/color-utils'; + +const directionMap = { + A: 'asc', + D: 'desc', +}; + +const MAX_CELLS = 10000; + +export function getHighestPossibleRpp(width, rowsPerPageOptions) { + const highestPossibleOption = [...rowsPerPageOptions].reverse().find((opt) => opt * width <= MAX_CELLS); + return highestPossibleOption || Math.floor(MAX_CELLS / width); // covering corner case of lowest option being too high +} + +export function getColumnOrder({ qColumnOrder, qDimensionInfo, qMeasureInfo }) { + const columnsLength = qDimensionInfo.length + qMeasureInfo.length; + return qColumnOrder?.length === columnsLength ? qColumnOrder : [...Array(columnsLength).keys()]; +} + +/** + * Get total cell info + * @param {Boolean} isDim + * @param {Object} layout + * @param {Number} colIndex + * @param {Number} numDims + * @returns dimensions and measures total cell values as strings + */ +export function getTotalInfo(isDim, layout, colIndex, numDims, columnOrder) { + if (!isDim) return layout.qHyperCube.qGrandTotalRow[colIndex - numDims]?.qText; + if (colIndex === 0 && columnOrder[0] === 0) return layout.totals.label; + return ''; +} + +function getRepresenstation(isDim, info) { + return { + ...info.representation, + globalMax: isDim ? undefined : info?.qMiniChart?.qYMax, + globalMin: isDim ? undefined : info?.qMiniChart?.qYMin, + }; +} + +function getMinMaxMeasureInfo(isDim, info) { + return isDim + ? undefined + : { + qMax: info?.qMax, + qMin: info?.qMin, + }; +} +function initConidtionalColor(isDim, info) { + if (!isDim && info.conditionalColoring) { + const cloned = JSON.parse(JSON.stringify(info.conditionalColoring)); + cloned?.segments?.limits.sort((a, b) => a?.value - b?.value); + return cloned; + } + return undefined; +} + +export function getColumnInfo(layout, colIndex, columnOrder) { + const { qDimensionInfo, qMeasureInfo } = layout.qHyperCube; + const numDims = qDimensionInfo.length; + const isDim = colIndex < numDims; + const info = isDim ? qDimensionInfo[colIndex] : qMeasureInfo[colIndex - numDims]; + const isHidden = info.qError?.qErrorCode === 7005; + const isLocked = info.qLocked; + const autoAlign = isDim ? 'left' : 'right'; + + return ( + !isHidden && { + isDim, + isLocked, + width: 200, + label: info.qFallbackTitle, + id: `col-${colIndex}`, + align: !info.textAlign || info.textAlign.auto ? autoAlign : info.textAlign.align, + stylingInfo: info.qAttrExprInfo.map((expr) => expr.id ?? ''), // need to coalesce to string value for native decode + sortDirection: directionMap[info.qSortIndicator], + dataColIdx: colIndex, + totalInfo: getTotalInfo(isDim, layout, colIndex, numDims, columnOrder), + conditionalColoring: initConidtionalColor(isDim, info), + representation: getRepresenstation(isDim, info), + ...getMinMaxMeasureInfo(isDim, info), + } + ); +} + +/** + * Get the total head position of the table + * + * @param {Object} layout + * @returns the position as a string, it can be any of top, bottom or noTotals + */ +export function getTotalPosition(layout) { + const [hasOnlyMeasure, hasDimension, hasGrandTotal, hasMeasure, isTotalModeAuto] = [ + layout.qHyperCube.qDimensionInfo.length === 0, + layout.qHyperCube.qDimensionInfo.length > 0, + layout.qHyperCube.qGrandTotalRow.length > 0, + layout.qHyperCube.qMeasureInfo.length > 0, + layout.totals?.show, + ]; + + if (hasGrandTotal && ((hasDimension && hasMeasure) || (!isTotalModeAuto && hasOnlyMeasure))) { + if (isTotalModeAuto || (!isTotalModeAuto && layout.totals.position === 'top')) return 'top'; + if (!isTotalModeAuto && layout.totals.position === 'bottom') return 'bottom'; + } + return 'noTotals'; +} + +const getExpressionColor = (col, row, key) => { + if (col.stylingInfo?.length > 0 && row.qAttrExps?.qValues?.length > 0) { + const bgIndex = col.stylingInfo.indexOf(key); + if (bgIndex !== -1 && bgIndex < row.qAttrExps.qValues.length) { + const rgbString = resolveToARGBorRGB(row.qAttrExps.qValues[bgIndex].qText); + if (rgbString !== 'none') { + const count = rgbString.startsWith("argb") == true ? 5 : 4; + return `#${rgbString + .slice(count, -1) + .split(',') + .map((x) => (+x).toString(16).padStart(2, 0)) + .join('')}`; + } + } + } + return undefined; +}; + + +export function resolveToARGBorRGB(input) { + // rgb + let matches = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(input); + if (matches) { + return `rgb(${matches[1]},${matches[2]},${matches[3]})`; + } + // rgba + matches = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d(\.\d+)?)\s*\)$/i.exec(input); + if (matches) { + return `argb(${matches[2]},${matches[3]},${matches[4]},${matches[1]})`; + } + // argb + matches = /^argb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(input); + if (matches) { + //const a = Math.round(matches[1] / 2.55) / 100; + return `argb(${matches[1]},${matches[2]},${matches[3]},${matches[4]})`; + } + // hex (#rgb, #rgba, #rrggbb, and #rrggbbaa) + matches = /^#(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/i.exec(input); + if (matches) { + return hexToRGBAorRGB(input); + } + // css color + const color = input && cssColors[input.toLowerCase()]; + if (color) { + return typeof color.a !== 'undefined' + ? `rgba(${color.r},${color.g},${color.b},${color.a})` + : `rgb(${color.r},${color.g},${color.b})`; + } + // invalid + return 'none'; +} + +const getCellBackgroundColor = (col, row) => { + const cellBackgroundColor = getExpressionColor(col, row, 'cellBackgroundColor'); + let cellForegroundColor = getExpressionColor(col, row, 'cellForegroundColor'); + if (cellBackgroundColor && isDarkColor(cellBackgroundColor) && !cellForegroundColor) { + cellForegroundColor = '#FFFFFF'; + } + return { cellBackgroundColor, cellForegroundColor }; +}; + +const validateImageUrl = (row, col, colIdx, imageOrigins) => { + if (col?.representation?.imageUrl) { + const { imageUrl } = col.representation; + const allowed = imageOrigins.some((s) => imageUrl.includes(s)); + if (!allowed) { + col.representation.imageUrl = null; + } + } + if (col?.stylingInfo) { + const urlIndex = col.stylingInfo?.indexOf('imageUrl'); + if (urlIndex !== -1) { + const url = row[colIdx].qAttrExps?.qValues?.[urlIndex].qText; + const allowed = imageOrigins.some((s) => url.includes(s)); + if (!allowed) { + row[colIdx].qAttrExps.qValues[urlIndex].qText = null; + } + } + } +}; + +export default async function manageData(model, layout, pageInfo, setPageInfo, qaeProps) { + const { page, rowsPerPage, rowsPerPageOptions } = pageInfo; + const { qHyperCube } = layout; + const totalColumnCount = qHyperCube.qSize.qcx; + const totalRowCount = qHyperCube.qSize.qcy; + const totalPages = Math.ceil(totalRowCount / rowsPerPage); + + const paginationNeeded = totalRowCount > 10; // TODO: This might not be true if you have > 1000 columns + const top = page * rowsPerPage; + const height = Math.min(rowsPerPage, totalRowCount - top); + // When the number of rows is reduced (e.g. confirming selections), + // you can end up still being on a page that doesn't exist anymore, then go back to the first page and return null + if (page > 0 && top >= totalRowCount && pageInfo) { + setPageInfo({ ...pageInfo, page: 0 }); + return null; + } + // If the number of cells exceeds 10k then we need to lower the rows per page to the maximum possible value + if (height * totalColumnCount > MAX_CELLS && pageInfo) { + setPageInfo({ ...pageInfo, rowsPerPage: getHighestPossibleRpp(totalColumnCount, rowsPerPageOptions), page: 0 }); + return null; + } + + const columnOrder = getColumnOrder(qHyperCube); + // using filter to remove hidden columns (represented with false) + const columns = columnOrder.map((colIndex) => getColumnInfo(layout, colIndex, columnOrder)).filter(Boolean); + const dataPages = await model.getHyperCubeData('/qHyperCubeDef', [ + { qTop: top, qLeft: 0, qHeight: height, qWidth: totalColumnCount }, + ]); + + const rows = dataPages[0]?.qMatrix.map((r, rowIdx) => { + const row = { key: `row-${rowIdx}` }; + columns.forEach((c, colIdx) => { + validateImageUrl(r, c, colIdx, qaeProps.imageOrigins); + row[c.id] = { + ...r[colIdx], + rowIdx: rowIdx + top, + colIdx: columnOrder[colIdx], + isSelectable: c.isDim && !c.isLocked, + rawRowIdx: rowIdx, + rawColIdx: colIdx, + prevQElemNumber: dataPages[0].qMatrix[rowIdx - 1]?.[colIdx]?.qElemNumber, + nextQElemNumber: dataPages[0].qMatrix[rowIdx + 1]?.[colIdx]?.qElemNumber, + indicator: getIndicator(c, r[colIdx]), + ...getCellBackgroundColor(c, r[colIdx]), + }; + }); + return row; + }); + const totalsPosition = getTotalPosition(layout); + return { totalColumnCount, totalRowCount, totalPages, paginationNeeded, rows, columns, totalsPosition }; +} diff --git a/prebuilt/react-native-sn-table/index.js b/prebuilt/react-native-sn-table/index.js new file mode 100644 index 00000000..b4545a20 --- /dev/null +++ b/prebuilt/react-native-sn-table/index.js @@ -0,0 +1,149 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable react-hooks/rules-of-hooks */ +import { + useElement, + useStaleLayout, + useEffect, + useOptions, + useModel, + useState, + useConstraints, + useTranslator, + useSelections, + usePromise, + useKeyboard, + useRect, + useApp, +} from '@nebula.js/stardust'; + +import properties from './object-properties'; +import data from './data'; +import ext from './ext'; +import manageData from './handle-data'; +import { render, teardown } from './table/Root'; +import useReactRoot from './nebula-hooks/use-react-root'; +import useAnnounceAndTranslations from './nebula-hooks/use-announce-and-translations'; +import useSorting from './nebula-hooks/use-sorting'; +import useExtendedTheme from './nebula-hooks/use-extended-theme'; + +const initialPageInfo = { + page: 0, + rowsPerPage: 100, + rowsPerPageOptions: [10, 25, 100], +}; +const nothing = async () => {}; +const renderWithCarbon = ({ + env, + rootElement, + model, + theme, + selectionsAPI, + app, + rect, + layout, + changeSortOrder, + translator, +}) => { + if (env.carbon && changeSortOrder && theme) { + render(rootElement, { + layout, + model, + manageData, + theme, + selectionsAPI, + changeSortOrder, + app, + rect, + translator, + qaeProps: env.qaeProps, + }); + } +}; + +export default function supernova(env) { + return { + qae: { + properties: { initial: properties }, + data: data(), + }, + ext: ext(env), + component() { + const rootElement = useElement(); + const reactRoot = useReactRoot(rootElement); + const layout = useStaleLayout(); + const { direction, footerContainer } = useOptions(); + const app = useApp(); + const model = useModel(); + const constraints = useConstraints(); + const translator = useTranslator(); + const selectionsAPI = useSelections(); + const keyboard = useKeyboard(); + const rect = useRect(); + const theme = useExtendedTheme(rootElement); + const announce = useAnnounceAndTranslations(rootElement, translator); + const changeSortOrder = useSorting(model); + + const [pageInfo, setPageInfo] = useState(initialPageInfo); + const [tableData] = usePromise(() => { + return env.carbon ? nothing() : manageData(model, layout, pageInfo, setPageInfo); + }, [layout, pageInfo]); + + useEffect(() => { + if (!env.carbon && reactRoot && layout && tableData && announce && changeSortOrder && theme && setPageInfo) { + render(reactRoot, { + rootElement, + layout, + tableData, + direction, + pageInfo, + setPageInfo, + constraints, + translator, + selectionsAPI, + theme, + changeSortOrder, + keyboard, + rect, + footerContainer, + announce, + }); + } + }, [ + reactRoot, + tableData, + constraints, + direction, + selectionsAPI.isModal(), + theme, + keyboard.active, + rect.width, + announce, + changeSortOrder, + setPageInfo, + ]); + + // this is the one we want to use for carbon + useEffect(() => { + renderWithCarbon({ + env, + rootElement, + model, + theme, + selectionsAPI, + app, + rect, + layout, + changeSortOrder, + translator, + }); + }, [layout, model, selectionsAPI.isModal(), theme, translator.language(), app, changeSortOrder, translator]); + + useEffect( + () => () => { + reactRoot && teardown(reactRoot); + }, + [reactRoot] + ); + }, + }; +} diff --git a/prebuilt/react-native-sn-table/locale/README.md b/prebuilt/react-native-sn-table/locale/README.md new file mode 100644 index 00000000..f4fe6ec2 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/README.md @@ -0,0 +1,42 @@ +# Locale + +## The core idea + +Registers a string in multiple locales + +```js +translator.add({ + id: 'company.hello_user', + locale: { + 'en-US': 'Hello {0}', + 'sv-SE': 'Hej {0}', + }, +}); +translator.get('company.hello_user', ['John']); // Hello John +``` + +## Command + +Generate all.json + +```sh +yarn locale:generate +``` + +verify locale + +```sh +yarn locale:verify +``` + +## Locale specific strings + +The English resource (string) container [en.json](./locales/en.json) on `main` branch is monitored for changes. All modifications will automatically be picked up and propagated to the other locale files by the Globalization Services team at Qlik at regular intervals (normally at least twice a week). + +## Pull requests touching English string resources + +Please request a review from a technical writer on all PRs touching English string resources, to ensure changes are consistent with style guides and translatability requirements. + +## Important + +Any changes to **non-English files** will be overwritten on next translation delivery. If you need to modify the non-English strings for any reason, please contact Qlik Globalization Services via [@qlik-oss/globalization](https://github.com/orgs/qlik-oss/teams/globalization). diff --git a/prebuilt/react-native-sn-table/locale/__tests__/translations.spec.js b/prebuilt/react-native-sn-table/locale/__tests__/translations.spec.js new file mode 100644 index 00000000..8fe1ae25 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/__tests__/translations.spec.js @@ -0,0 +1,37 @@ +import registerLocale from '../src/index'; +// eslint-disable-next-line import/no-unresolved +import all from '../all.json'; + +describe('translations', () => { + describe('registerLocale', () => { + let translator; + beforeEach(() => { + translator = { + get: (t) => t === 'SNTable.Accessibility.RowsAndColumns' && 'SNTable.Accessibility.RowsAndColumns', + add: jest.fn(), + }; + }); + + it('Should not add anything when translator is not passed', () => { + registerLocale(); + expect(translator.add).not.toHaveBeenCalled(); + }); + + it('Should not add anything when get is undefined', () => { + translator.get = undefined; + registerLocale(translator); + expect(translator.add).not.toHaveBeenCalled(); + }); + + it('Should early return when translation is different from id', () => { + translator.get = () => 'someTranslation'; + registerLocale(translator); + expect(translator.add).not.toHaveBeenCalled(); + }); + + it('Should call add for every key', () => { + registerLocale(translator); + expect(translator.add).toHaveBeenCalledTimes(Object.keys(all).length); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/locale/all.json b/prebuilt/react-native-sn-table/locale/all.json new file mode 100644 index 00000000..3fb2432f --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/all.json @@ -0,0 +1,442 @@ +{ + "SNTable_Accessibility_NavigationInstructions": { + "id": "SNTable.Accessibility.NavigationInstructions", + "locale": { + "de-DE": "Verwenden Sie die Pfeiltasten, um in den Tabellenzellen zu navigieren, und die Tabulatortaste, um zu den Paginierungssteuerungen zu wechseln. Alle Angaben zur Tastaturnavigation finden Sie in der Dokumentation.", + "en-US": "Use arrow keys to navigate in table cells and tab to move to pagination controls. For the full range of keyboard navigation, see the documentation.", + "es-ES": "Use las teclas de flecha para navegar en las celdas de la tabla y la pestaña para moverse a los controles de paginación. Para conocer la gama completa de navegación con teclado, consulte la documentación.", + "fr-FR": "Utilisez les touches fléchées pour parcourir les cellules du tableau et la tabulation pour accéder aux commandes de pagination. Pour connaître la gamme complète des commandes de navigation au clavier, voir la documentation.", + "it-IT": "Utilizzare i tasti freccia per navigare nelle celle della tabella e il tasto TAB per passare ai controlli di paginazione. Per informazioni complete sulla navigazione da tastiera, vedere la documentazione.", + "ja-JP": "矢印キーで表のセル内を移動し、タブ キーでページネーション コントロールに移動します。キーボード ナビゲーションの全範囲については、ドキュメントをご覧ください。", + "ko-KR": "화살표 키를 사용하여 테이블 셀을 탐색하고 탭을 사용하여 페이지 매김 컨트롤로 이동합니다. 전체 키보드 탐색 범위는 설명서를 참조하십시오.", + "nl-NL": "Gebruik de pijltjestoetsen om te navigeren in tabelcellen en de Tab-toets om naar besturingselementen voor paginering te gaan. Raadpleeg de documentatie voor een volledig overzicht van de toetsenbordnavigatie.", + "pl-PL": "Użyj klawiszy strzałek do poruszania się po komórkach tabeli oraz tabulatora do poruszania się po elementach sterujących stronami. Pełne informacje o nawigacji przy użyciu klawiatury zawiera dokumentacja.", + "pt-BR": "Use as setas para navegar nas células da tabela e Tab para mover para os controles de paginação. Para toda a gama de navegação do teclado, consulte a documentação.", + "ru-RU": "Используйте клавиши со стрелками для перемещения по ячейкам таблицы и клавишу табуляции для перехода к элементам управления разбивки на страницы. Полный перечень возможностей навигации с помощью клавиатуры см. в документации.", + "sv-SE": "Använd piltangenterna för att navigera i tabellcellerna och tabbtangenten för att flytta till pagineringskontrollerna. Se dokumentationen för komplett information om tangentbordsnavigering.", + "tr-TR": "Tablo hücrelerinde gezinmek için ok tuşlarını, sayfalandırma kontrollerine geçmek için sekmeyi kullanın. Tam kapsamlı klavye gezintisi için belgelere bakın.", + "zh-CN": "使用箭头键在表格单元格中导航,使用 tab 键移动到分页控件。有关键盘导航的完整范围,请参阅文档。", + "zh-TW": "使用方向鍵以在表格儲存格中導覽,並使用 Tab 以前往分頁控制。如需完整的鍵盤導覽,請參閱文件。" + } + }, + "SNTable_Accessibility_RowsAndColumns": { + "id": "SNTable.Accessibility.RowsAndColumns", + "locale": { + "de-DE": "Es werden {0} Zeilen und {1} Spalten angezeigt.", + "en-US": "Showing {0} rows and {1} columns.", + "es-ES": "Mostrando {0} filas y {1} columnas.", + "fr-FR": "Affichage de {0} lignes et de {1} colonnes.", + "it-IT": "Visualizzazione di {0} righe e {1} colonne.", + "ja-JP": "{0} 行 {1} 列を表示しています。", + "ko-KR": "{0}개의 행과 {1}개의 열을 표시합니다.", + "nl-NL": "{0} rijen en {1} kolommen worden getoond.", + "pl-PL": "Wyświetlane wiersze: {0} i kolumny: {1}.", + "pt-BR": "Mostrando {0} linhas e {1} colunas.", + "ru-RU": "Показаны строки ({0}) и столбцы ({1}).", + "sv-SE": "Visar {0} rader och {1} kolumner.", + "tr-TR": "{0} satır ve {1} sütun gösteriliyor.", + "zh-CN": "正在显示 {0} 行,{1} 列。", + "zh-TW": "顯示 {0} 列和 {1} 欄。" + } + }, + "SNTable_Pagination_DisplayedRowsLabel": { + "id": "SNTable.Pagination.DisplayedRowsLabel", + "locale": { + "de-DE": "{0} von {1}", + "en-US": "{0} of {1}", + "es-ES": "{0} de {1}", + "fr-FR": "{0} sur {1}", + "it-IT": "{0} di {1}", + "ja-JP": "{0} / {1}", + "ko-KR": "{1}의 {0}", + "nl-NL": "{0} van {1}", + "pl-PL": "{0} z {1}", + "pt-BR": "{0} de {1}", + "ru-RU": "{0} из {1}", + "sv-SE": "{0} av {1}", + "tr-TR": "{0} / {1}", + "zh-CN": "{0} / {1}", + "zh-TW": "{0} / {1}" + } + }, + "SNTable_Pagination_FirstPage": { + "id": "SNTable.Pagination.FirstPage", + "locale": { + "de-DE": "Zur ersten Seite", + "en-US": "Go to the first page", + "es-ES": "Ir a la primera página", + "fr-FR": "Accéder à la première page", + "it-IT": "Vai alla prima pagina", + "ja-JP": "最初のページに移動", + "ko-KR": "첫 페이지로 이동", + "nl-NL": "Ga naar de eerste pagina", + "pl-PL": "Przejdź do pierwszej strony", + "pt-BR": "Ir para a primeira página", + "ru-RU": "Перейти к первой странице", + "sv-SE": "Gå till första sidan", + "tr-TR": "İlk sayfaya git", + "zh-CN": "转到第一页", + "zh-TW": "移至第一頁" + } + }, + "SNTable_Pagination_LastPage": { + "id": "SNTable.Pagination.LastPage", + "locale": { + "de-DE": "Zur letzten Seite", + "en-US": "Go to the last page", + "es-ES": "Ir a la última página", + "fr-FR": "Accéder à la dernière page", + "it-IT": "Vai all'ultima pagina", + "ja-JP": "最後のページに移動", + "ko-KR": "마지막 페이지로 이동", + "nl-NL": "Ga naar de laatste pagina", + "pl-PL": "Przejdź do ostatniej strony", + "pt-BR": "Ir para a última página", + "ru-RU": "Перейти к последней странице", + "sv-SE": "Gå till sista sidan", + "tr-TR": "Son sayfaya git", + "zh-CN": "转到最后一页", + "zh-TW": "移至最後一頁" + } + }, + "SNTable_Pagination_NextPage": { + "id": "SNTable.Pagination.NextPage", + "locale": { + "de-DE": "Zur nächsten Seite", + "en-US": "Go to the next page", + "es-ES": "Ir a la página siguiente", + "fr-FR": "Accéder à la page suivante", + "it-IT": "Vai alla pagina successiva", + "ja-JP": "次のページに移動", + "ko-KR": "다음 페이지로 이동", + "nl-NL": "Ga naar de volgende pagina", + "pl-PL": "Przejdź do następnej strony", + "pt-BR": "Ir para a próxima página", + "ru-RU": "Перейти к следующей странице", + "sv-SE": "Gå till nästa sida", + "tr-TR": "Sonraki sayfaya git", + "zh-CN": "转到下一页", + "zh-TW": "移至下一頁" + } + }, + "SNTable_Pagination_PageStatusReport": { + "id": "SNTable.Pagination.PageStatusReport", + "locale": { + "de-DE": "Die Seite wurde geändert. Seite {0} von {1} wird angezeigt.", + "en-US": "Page has changed. Showing page {0} of {1}.", + "es-ES": "La página ha cambiado. Mostrando página {0} de {1}.", + "fr-FR": "La page a changé. Affichage de la page {0} sur {1}.", + "it-IT": "La pagina è cambiata. Visualizzazione pagina {0} di {1}.", + "ja-JP": "ページが変更されました。{1} ページ中の {0} ページ目を表示しています。", + "ko-KR": "페이지가 변경되었습니다. {1}의 {0} 페이지를 보여 줍니다.", + "nl-NL": "Pagina is gewijzigd. Pagina {0} van {1} wordt weergeven.", + "pl-PL": "Strona się zmieniła Wyświetlanie strony {0} z {1}.", + "pt-BR": "A página mudou. Mostrando página {0} de {1}.", + "ru-RU": "Страница изменилась. Показана страница {0} из {1}.", + "sv-SE": "Sidan har ändrats. Visar sida {0} av {1}.", + "tr-TR": "Sayfa değişti. Sayfa {0} / {1} gösteriliyor.", + "zh-CN": "页面已更改。显示页面 {0} / {1}。", + "zh-TW": "頁面已變更。顯示第 {0} 頁,共 {1} 頁。" + } + }, + "SNTable_Pagination_PreviousPage": { + "id": "SNTable.Pagination.PreviousPage", + "locale": { + "de-DE": "Zur vorherigen Seite", + "en-US": "Go to the previous page", + "es-ES": "Ir a la página anterior", + "fr-FR": "Accéder à la page précédente", + "it-IT": "Vai alla pagina precedente", + "ja-JP": "前のページに移動", + "ko-KR": "이전 페이지로 이동", + "nl-NL": "Ga naar de vorige pagina", + "pl-PL": "Przejdź do poprzedniej strony", + "pt-BR": "Ir para a página anterior", + "ru-RU": "Перейти к предыдущей странице", + "sv-SE": "Gå till föregående sida", + "tr-TR": "Önceki sayfaya git", + "zh-CN": "转到前一页", + "zh-TW": "移至上一頁" + } + }, + "SNTable_Pagination_RowsPerPage": { + "id": "SNTable.Pagination.RowsPerPage", + "locale": { + "de-DE": "Zeilen pro Seite", + "en-US": "Rows per page", + "es-ES": "Filas por página", + "fr-FR": "Lignes par page", + "it-IT": "Righe per pagina", + "ja-JP": "ページあたりの行数", + "ko-KR": "페이지별 행 수", + "nl-NL": "Rijen per pagina", + "pl-PL": "Wierszy na stronę", + "pt-BR": "Linhas por página", + "ru-RU": "Строк на странице", + "sv-SE": "Rader per sida", + "tr-TR": "Sayfa başına satır sayısı", + "zh-CN": "每页行数", + "zh-TW": "每頁列數" + } + }, + "SNTable_Pagination_RowsPerPageChange": { + "id": "SNTable.Pagination.RowsPerPageChange", + "locale": { + "de-DE": "Zeilen pro Seite wurden in {0} geändert. Jetzt wird die erste Seite angezeigt.", + "en-US": "Rows per page has changed to {0}. Now showing the first page.", + "es-ES": "El número de filas por página ha cambiado a {0}. Mostrando ahora la primera página.", + "fr-FR": "Le nombre de lignes par page a été remplacé par {0}. Affichage en cours de la première page.", + "it-IT": "Le righe per pagina sono cambiate a {0}. Viene ora mostrata la prima pagina.", + "ja-JP": "ページあたりの行数が {0} に変更されました。現在、最初のページを表示しています。", + "ko-KR": "페이지당 행 수가 {0}(으)로 변경되었습니다. 이제 첫 페이지를 보여 줍니다.", + "nl-NL": "Aantal rijen per pagina is gewijzigd naar {0}. De eerste pagina wordt nu weergegeven.", + "pl-PL": "Liczba wierszy na stronę została zmieniona na {0}. Wyświetlana jest pierwsza strona.", + "pt-BR": "Linhas por página mudou para {0}. Agora mostrando a primeira página.", + "ru-RU": "Количество строк на странице изменилось на {0}. Теперь отображается первая страница.", + "sv-SE": "Rader per sida har ändrats till {0}. Nu visas första sidan.", + "tr-TR": "Sayfa başına satır sayısı {0} olarak değiştirildi. Şimdi ilk sayfa gösteriliyor.", + "zh-CN": "每页行数已更改为 {0}。现在显示第一页。", + "zh-TW": "每頁列數已變更為 {0}。現在顯示第一頁。" + } + }, + "SNTable_Pagination_SelectPage": { + "id": "SNTable.Pagination.SelectPage", + "locale": { + "de-DE": "Seite auswählen", + "en-US": "Select page", + "es-ES": "Seleccionar página", + "fr-FR": "Sélectionner une page", + "it-IT": "Seleziona pagina", + "ja-JP": "ページを選択", + "ko-KR": "페이지 선택", + "nl-NL": "Pagina selecteren", + "pl-PL": "Wybierz stronę", + "pt-BR": "Selecionar página", + "ru-RU": "Выберите страницу", + "sv-SE": "Välj sida", + "tr-TR": "Sayfa seç", + "zh-CN": "选择页面", + "zh-TW": "選取頁面" + } + }, + "SNTable_SelectionLabel_DeselectedValue": { + "id": "SNTable.SelectionLabel.DeselectedValue", + "locale": { + "de-DE": "Die Werteauswahl ist aufgehoben.", + "en-US": "Value is deselected.", + "es-ES": "El valor no está seleccionado.", + "fr-FR": "La valeur est désélectionnée.", + "it-IT": "Il valore è deselezionato.", + "ja-JP": "値が選択解除されました。", + "ko-KR": "값이 선택 취소되었습니다.", + "nl-NL": "Selectie van waarde opgeheven.", + "pl-PL": "Wartość nie jest zaznaczona.", + "pt-BR": "O valor está desmarcado.", + "ru-RU": "Выбор значения отменен.", + "sv-SE": "Val av värde har tagits bort.", + "tr-TR": "Değer seçimi iptal edildi.", + "zh-CN": "值已取消选择。", + "zh-TW": "值已取消選取。" + } + }, + "SNTable_SelectionLabel_ExitedSelectionMode": { + "id": "SNTable.SelectionLabel.ExitedSelectionMode", + "locale": { + "de-DE": "Die Werteauswahl ist aufgehoben. Auswahlmodus beendet.", + "en-US": "Value is deselected. Exited selection mode.", + "es-ES": "El valor no está seleccionado. Salió del modo de selección.", + "fr-FR": "La valeur est désélectionnée. Mode de sélection désactivé.", + "it-IT": "Il valore è deselezionato. Uscita dalla modalità selezione.", + "ja-JP": "値が選択解除されました。選択モードを終了しました。", + "ko-KR": "값이 선택 취소되었습니다. 선택 모드를 종료했습니다.", + "nl-NL": "Selectie van waarde opgeheven. Selectiemodus afgesloten.", + "pl-PL": "Wartość nie jest zaznaczona. Zakończono tryb wyboru.", + "pt-BR": "O valor está desmarcado. Modo de seleção encerrado.", + "ru-RU": "Выбор значения отменен. Выполнен выход из режима выборки.", + "sv-SE": "Val av värde har tagits bort. Lämnade urvalsläge.", + "tr-TR": "Değer seçimi iptal edildi. Seçim modundan çıkıldı.", + "zh-CN": "值已取消选择。已退出选择模式。", + "zh-TW": "值已取消選取。輸入的選取模式。" + } + }, + "SNTable_SelectionLabel_NotSelectedValue": { + "id": "SNTable.SelectionLabel.NotSelectedValue", + "locale": { + "de-DE": "Wert ist nicht ausgewählt.", + "en-US": "Value is not selected.", + "es-ES": "El valor no está seleccionado.", + "fr-FR": "La valeur n'est pas sélectionnée.", + "it-IT": "Il valore non è selezionato.", + "ja-JP": "値が選択されていません。", + "ko-KR": "값이 선택되지 않았습니다.", + "nl-NL": "Waarde is niet geselecteerd.", + "pl-PL": "Wartość nie jest wybrana.", + "pt-BR": "O valor não está selecionado.", + "ru-RU": "Значение не выбрано.", + "sv-SE": "Värdet är inte valt.", + "tr-TR": "Değer seçilmedi.", + "zh-CN": "未选择值。", + "zh-TW": "未選取值。" + } + }, + "SNTable_SelectionLabel_OneSelectedValue": { + "id": "SNTable.SelectionLabel.OneSelectedValue", + "locale": { + "de-DE": "Aktuell ist ein Wert ausgewählt.", + "en-US": "Currently there is one selected value.", + "es-ES": "Actualmente hay un valor seleccionado.", + "fr-FR": "Actuellement, une valeur est sélectionnée.", + "it-IT": "Attualmente è presente un solo valore selezionato.", + "ja-JP": "現在、値が 1 つ選択されています。", + "ko-KR": "현재 하나의 값이 선택되었습니다.", + "nl-NL": "Er is momenteel één waarde geselecteerd.", + "pl-PL": "Aktualnie jest jedna wybrana wartość.", + "pt-BR": "Atualmente, há um valor selecionado.", + "ru-RU": "В настоящее время выбрано одно значение.", + "sv-SE": "Just nu är ett värde valt.", + "tr-TR": "Şu anda seçili bir değer var.", + "zh-CN": "当前有一个已选择的值。", + "zh-TW": "目前有一個選取的值。" + } + }, + "SNTable_SelectionLabel_SelectedValue": { + "id": "SNTable.SelectionLabel.SelectedValue", + "locale": { + "de-DE": "Wert ist ausgewählt.", + "en-US": "Value is selected.", + "es-ES": "El valor está seleccionado.", + "fr-FR": "La valeur est sélectionnée.", + "it-IT": "Il valore è selezionato.", + "ja-JP": "値が選択されました。", + "ko-KR": "값이 선택되었습니다.", + "nl-NL": "Waarde is geselecteerd.", + "pl-PL": "Wartość jest wybrana.", + "pt-BR": "O valor está selecionado.", + "ru-RU": "Значение выбрано.", + "sv-SE": "Värdet är valt.", + "tr-TR": "Değer seçildi.", + "zh-CN": "已选择值。", + "zh-TW": "已選取值。" + } + }, + "SNTable_SelectionLabel_SelectedValues": { + "id": "SNTable.SelectionLabel.SelectedValues", + "locale": { + "de-DE": "Aktuell sind {0} Werte ausgewählt.", + "en-US": "Currently there are {0} selected values.", + "es-ES": "Actualmente hay {0} valores seleccionados.", + "fr-FR": "Actuellement, {0} valeurs sont sélectionnées.", + "it-IT": "Attualmente sono presenti {0} valori selezionati.", + "ja-JP": "現在、値が {0} つ選択されています。", + "ko-KR": "현재 {0}개의 값이 선택되었습니다.", + "nl-NL": "Er zijn momenteel {0} geselecteerde waarden.", + "pl-PL": "Liczba aktualnie wybranych wartości: {0}.", + "pt-BR": "Atualmente, há {0} valores selecionados.", + "ru-RU": "В настоящее время выбраны значения: {0}.", + "sv-SE": "Just nu är {0} värden valda.", + "tr-TR": "Şu anda seçili {0} değer var.", + "zh-CN": "当前有 {0} 个已选择的值。", + "zh-TW": "目前有 {0} 個選取的值。" + } + }, + "SNTable_SelectionLabel_SelectionsConfirmed": { + "id": "SNTable.SelectionLabel.SelectionsConfirmed", + "locale": { + "de-DE": "Auswahl bestätigt.", + "en-US": "Selections confirmed.", + "es-ES": "Confirmadas las selecciones.", + "fr-FR": "Sélections confirmées.", + "it-IT": "Selezioni confermate.", + "ja-JP": "選択が確定されました。", + "ko-KR": "선택 내용이 확인되었습니다.", + "nl-NL": "Selecties bevestigd.", + "pl-PL": "Potwierdzono wybory.", + "pt-BR": "Seleções confirmadas.", + "ru-RU": "Выборки подтверждены.", + "sv-SE": "Valen har bekräftats.", + "tr-TR": "Seçimler onaylandı.", + "zh-CN": "选择已确认。", + "zh-TW": "選項已確認。" + } + }, + "SNTable_SelectionLabel_selected": { + "id": "SNTable.SelectionLabel.selected", + "locale": { + "de-DE": "Ausgewählt", + "en-US": "Selected", + "es-ES": "Seleccionado", + "fr-FR": "Sélectionné", + "it-IT": "Selezionato", + "ja-JP": "選択済み", + "ko-KR": "선택됨", + "nl-NL": "Geselecteerd", + "pl-PL": "Wybrane", + "pt-BR": "Selecionado", + "ru-RU": "Выбрано", + "sv-SE": "Urval", + "tr-TR": "Seçildi", + "zh-CN": "已选择", + "zh-TW": "已選取" + } + }, + "SNTable_SortLabel_PressSpaceToSort": { + "id": "SNTable.SortLabel.PressSpaceToSort", + "locale": { + "de-DE": "Drücken Sie die Leertaste, um nach dieser Spalte zu sortieren", + "en-US": "Press space to sort on this column", + "es-ES": "Pulse la barra espaciadora para ordenar en esta columna.", + "fr-FR": "Appuyez sur Espace pour trier en fonction de cette colonne.", + "it-IT": "Premere la barra spaziatrice per ordinare questa colonna", + "ja-JP": "スペース キーを押して、この列をソートします", + "ko-KR": "이 열을 정렬하려면 스페이스바를 누르십시오.", + "nl-NL": "Druk op de spatiebalk om sorteren in deze kolom in te schakelen", + "pl-PL": "Naciśnij spację, aby posortować według tej kolumny", + "pt-BR": "Pressione espaço para classificar por esta coluna", + "ru-RU": "Нажмите пробел, чтобы отсортировать по этому столбцу", + "sv-SE": "Tryck på Blanksteg om du vill sortera efter den här kolumnen", + "tr-TR": "Bu sütunda sıralama yapmak için boşluk tuşuna basın.", + "zh-CN": "按下空格键以在该列上排序。", + "zh-TW": "在此欄按下空格鍵以進行排序" + } + }, + "SNTable_SortLabel_SortedAscending": { + "id": "SNTable.SortLabel.SortedAscending", + "locale": { + "de-DE": "In aufsteigender Reihenfolge sortiert.", + "en-US": "Sorted in ascending order.", + "es-ES": "Ordenado por orden ascendente.", + "fr-FR": "Tri dans l'ordre croissant.", + "it-IT": "Ordinato in ordine crescente.", + "ja-JP": "昇順で並べ替えられます。", + "ko-KR": "오름차순으로 정렬되었습니다.", + "nl-NL": "Gesorteerd in oplopende volgorde.", + "pl-PL": "Posortowane rosnąco.", + "pt-BR": "Classificado em ordem crescente.", + "ru-RU": "Сортировка в порядке возрастания.", + "sv-SE": "Sorterat i stigande ordning.", + "tr-TR": "Artan düzende sıralanmıştır.", + "zh-CN": "已按升序排列。", + "zh-TW": "已依遞增順序排序。" + } + }, + "SNTable_SortLabel_SortedDescending": { + "id": "SNTable.SortLabel.SortedDescending", + "locale": { + "de-DE": "In absteigender Reihenfolge sortiert.", + "en-US": "Sorted in descending order.", + "es-ES": "Ordenado por orden descendente.", + "fr-FR": "Tri dans l'ordre décroissant.", + "it-IT": "Ordinato in ordine decrescente.", + "ja-JP": "降順で並べ替えられます。", + "ko-KR": "내림차순으로 정렬되었습니다.", + "nl-NL": "Gesorteerd in aflopende volgorde.", + "pl-PL": "Posortowane malejąco.", + "pt-BR": "Classificado em ordem decrescente.", + "ru-RU": "Сортировка в порядке убывания.", + "sv-SE": "Sorterat i fallande ordning.", + "tr-TR": "Azalan düzende sıralanmıştır.", + "zh-CN": "已按降序排列。", + "zh-TW": "已依遞減順序排序。" + } + } +} \ No newline at end of file diff --git a/prebuilt/react-native-sn-table/locale/locales/de.json b/prebuilt/react-native-sn-table/locale/locales/de.json new file mode 100644 index 00000000..0441ea17 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/de.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Verwenden Sie die Pfeiltasten, um in den Tabellenzellen zu navigieren, und die Tabulatortaste, um zu den Paginierungssteuerungen zu wechseln. Alle Angaben zur Tastaturnavigation finden Sie in der Dokumentation.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Es werden {0} Zeilen und {1} Spalten angezeigt.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} von {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Zur ersten Seite", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Zur letzten Seite", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Zur nächsten Seite", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "Die Seite wurde geändert. Seite {0} von {1} wird angezeigt.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Zur vorherigen Seite", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Zeilen pro Seite", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Zeilen pro Seite wurden in {0} geändert. Jetzt wird die erste Seite angezeigt.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Seite auswählen", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Die Werteauswahl ist aufgehoben.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Die Werteauswahl ist aufgehoben. Auswahlmodus beendet.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Wert ist nicht ausgewählt.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Aktuell ist ein Wert ausgewählt.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Wert ist ausgewählt.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Aktuell sind {0} Werte ausgewählt.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Auswahl bestätigt.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Ausgewählt", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Drücken Sie die Leertaste, um nach dieser Spalte zu sortieren", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "In aufsteigender Reihenfolge sortiert.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "In absteigender Reihenfolge sortiert.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/en.json b/prebuilt/react-native-sn-table/locale/locales/en.json new file mode 100644 index 00000000..75a2bc3a --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/en.json @@ -0,0 +1,90 @@ +{ + "SNTable.Accessibility.RowsAndColumns": { + "value": "Showing {0} rows and {1} columns.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)" + }, + "SNTable.Accessibility.NavigationInstructions": { + "value": "Use arrow keys to navigate in table cells and tab to move to pagination controls. For the full range of keyboard navigation, see the documentation.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Rows per page", + "comment": "label for the rows per page dropdown. (tew 210929)" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} of {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)" + }, + "SNTable.Pagination.SelectPage": { + "value": "Select page", + "comment": "Label for the dropdown to select a page." + }, + "SNTable.Pagination.FirstPage": { + "value": "Go to the first page", + "comment": "tooltip and aria-label for the first page button. (tew 220125)" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Go to the previous page", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)" + }, + "SNTable.Pagination.NextPage": { + "value": "Go to the next page", + "comment": "tooltip and aria-label for the next page button. (tew 220125)" + }, + "SNTable.Pagination.LastPage": { + "value": "Go to the last page", + "comment": "tooltip and aria-label for the last page button. (tew 220125)" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Rows per page has changed to {0}. Now showing the first page.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "Page has changed. Showing page {0} of {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Sorted in ascending order.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Sorted in descending order.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Press space to sort on this column", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Currently there is one selected value.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Currently there are {0} selected values.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Value is deselected.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Value is selected.", + "comment": "announced by screen readers when a value is selected. (tew 210929)" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Value is not selected.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Selections confirmed.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Value is deselected. Exited selection mode.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)" + }, + "SNTable.SelectionLabel.selected": { + "value": "Selected", + "comment": "announced by screen readers for a selected value. (tew 210929)" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/es.json b/prebuilt/react-native-sn-table/locale/locales/es.json new file mode 100644 index 00000000..2478f14e --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/es.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Use las teclas de flecha para navegar en las celdas de la tabla y la pestaña para moverse a los controles de paginación. Para conocer la gama completa de navegación con teclado, consulte la documentación.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Mostrando {0} filas y {1} columnas.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} de {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Ir a la primera página", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Ir a la última página", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Ir a la página siguiente", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "La página ha cambiado. Mostrando página {0} de {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Ir a la página anterior", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Filas por página", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "El número de filas por página ha cambiado a {0}. Mostrando ahora la primera página.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Seleccionar página", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "El valor no está seleccionado.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "El valor no está seleccionado. Salió del modo de selección.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "El valor no está seleccionado.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Actualmente hay un valor seleccionado.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "El valor está seleccionado.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Actualmente hay {0} valores seleccionados.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Confirmadas las selecciones.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Seleccionado", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Pulse la barra espaciadora para ordenar en esta columna.", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Ordenado por orden ascendente.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Ordenado por orden descendente.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/fr.json b/prebuilt/react-native-sn-table/locale/locales/fr.json new file mode 100644 index 00000000..b0f90a65 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/fr.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Utilisez les touches fléchées pour parcourir les cellules du tableau et la tabulation pour accéder aux commandes de pagination. Pour connaître la gamme complète des commandes de navigation au clavier, voir la documentation.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Affichage de {0} lignes et de {1} colonnes.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} sur {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Accéder à la première page", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Accéder à la dernière page", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Accéder à la page suivante", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "La page a changé. Affichage de la page {0} sur {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Accéder à la page précédente", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Lignes par page", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Le nombre de lignes par page a été remplacé par {0}. Affichage en cours de la première page.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Sélectionner une page", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "La valeur est désélectionnée.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "La valeur est désélectionnée. Mode de sélection désactivé.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "La valeur n'est pas sélectionnée.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Actuellement, une valeur est sélectionnée.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "La valeur est sélectionnée.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Actuellement, {0} valeurs sont sélectionnées.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Sélections confirmées.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Sélectionné", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Appuyez sur Espace pour trier en fonction de cette colonne.", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Tri dans l'ordre croissant.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Tri dans l'ordre décroissant.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/it.json b/prebuilt/react-native-sn-table/locale/locales/it.json new file mode 100644 index 00000000..a8913895 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/it.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Utilizzare i tasti freccia per navigare nelle celle della tabella e il tasto TAB per passare ai controlli di paginazione. Per informazioni complete sulla navigazione da tastiera, vedere la documentazione.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Visualizzazione di {0} righe e {1} colonne.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} di {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Vai alla prima pagina", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Vai all'ultima pagina", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Vai alla pagina successiva", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "La pagina è cambiata. Visualizzazione pagina {0} di {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Vai alla pagina precedente", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Righe per pagina", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Le righe per pagina sono cambiate a {0}. Viene ora mostrata la prima pagina.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Seleziona pagina", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Il valore è deselezionato.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Il valore è deselezionato. Uscita dalla modalità selezione.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Il valore non è selezionato.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Attualmente è presente un solo valore selezionato.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Il valore è selezionato.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Attualmente sono presenti {0} valori selezionati.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Selezioni confermate.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Selezionato", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Premere la barra spaziatrice per ordinare questa colonna", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Ordinato in ordine crescente.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Ordinato in ordine decrescente.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/ja.json b/prebuilt/react-native-sn-table/locale/locales/ja.json new file mode 100644 index 00000000..1cb52799 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/ja.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "矢印キーで表のセル内を移動し、タブ キーでページネーション コントロールに移動します。キーボード ナビゲーションの全範囲については、ドキュメントをご覧ください。", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "{0} 行 {1} 列を表示しています。", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} / {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "最初のページに移動", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "最後のページに移動", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "次のページに移動", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "ページが変更されました。{1} ページ中の {0} ページ目を表示しています。", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "前のページに移動", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "ページあたりの行数", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "ページあたりの行数が {0} に変更されました。現在、最初のページを表示しています。", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "ページを選択", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "値が選択解除されました。", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "値が選択解除されました。選択モードを終了しました。", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "値が選択されていません。", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "現在、値が 1 つ選択されています。", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "値が選択されました。", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "現在、値が {0} つ選択されています。", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "選択が確定されました。", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "選択済み", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "スペース キーを押して、この列をソートします", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "昇順で並べ替えられます。", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "降順で並べ替えられます。", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/ko.json b/prebuilt/react-native-sn-table/locale/locales/ko.json new file mode 100644 index 00000000..193a111b --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/ko.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "화살표 키를 사용하여 테이블 셀을 탐색하고 탭을 사용하여 페이지 매김 컨트롤로 이동합니다. 전체 키보드 탐색 범위는 설명서를 참조하십시오.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "{0}개의 행과 {1}개의 열을 표시합니다.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{1}의 {0}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "첫 페이지로 이동", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "마지막 페이지로 이동", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "다음 페이지로 이동", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "페이지가 변경되었습니다. {1}의 {0} 페이지를 보여 줍니다.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "이전 페이지로 이동", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "페이지별 행 수", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "페이지당 행 수가 {0}(으)로 변경되었습니다. 이제 첫 페이지를 보여 줍니다.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "페이지 선택", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "값이 선택 취소되었습니다.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "값이 선택 취소되었습니다. 선택 모드를 종료했습니다.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "값이 선택되지 않았습니다.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "현재 하나의 값이 선택되었습니다.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "값이 선택되었습니다.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "현재 {0}개의 값이 선택되었습니다.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "선택 내용이 확인되었습니다.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "선택됨", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "이 열을 정렬하려면 스페이스바를 누르십시오.", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "오름차순으로 정렬되었습니다.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "내림차순으로 정렬되었습니다.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/nl.json b/prebuilt/react-native-sn-table/locale/locales/nl.json new file mode 100644 index 00000000..3c871613 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/nl.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Gebruik de pijltjestoetsen om te navigeren in tabelcellen en de Tab-toets om naar besturingselementen voor paginering te gaan. Raadpleeg de documentatie voor een volledig overzicht van de toetsenbordnavigatie.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "{0} rijen en {1} kolommen worden getoond.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} van {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Ga naar de eerste pagina", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Ga naar de laatste pagina", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Ga naar de volgende pagina", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "Pagina is gewijzigd. Pagina {0} van {1} wordt weergeven.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Ga naar de vorige pagina", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Rijen per pagina", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Aantal rijen per pagina is gewijzigd naar {0}. De eerste pagina wordt nu weergegeven.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Pagina selecteren", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Selectie van waarde opgeheven.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Selectie van waarde opgeheven. Selectiemodus afgesloten.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Waarde is niet geselecteerd.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Er is momenteel één waarde geselecteerd.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Waarde is geselecteerd.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Er zijn momenteel {0} geselecteerde waarden.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Selecties bevestigd.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Geselecteerd", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Druk op de spatiebalk om sorteren in deze kolom in te schakelen", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Gesorteerd in oplopende volgorde.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Gesorteerd in aflopende volgorde.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/pl.json b/prebuilt/react-native-sn-table/locale/locales/pl.json new file mode 100644 index 00000000..a3cbf61f --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/pl.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Użyj klawiszy strzałek do poruszania się po komórkach tabeli oraz tabulatora do poruszania się po elementach sterujących stronami. Pełne informacje o nawigacji przy użyciu klawiatury zawiera dokumentacja.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Wyświetlane wiersze: {0} i kolumny: {1}.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} z {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Przejdź do pierwszej strony", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Przejdź do ostatniej strony", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Przejdź do następnej strony", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "Strona się zmieniła Wyświetlanie strony {0} z {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Przejdź do poprzedniej strony", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Wierszy na stronę", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Liczba wierszy na stronę została zmieniona na {0}. Wyświetlana jest pierwsza strona.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Wybierz stronę", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Wartość nie jest zaznaczona.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Wartość nie jest zaznaczona. Zakończono tryb wyboru.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Wartość nie jest wybrana.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Aktualnie jest jedna wybrana wartość.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Wartość jest wybrana.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Liczba aktualnie wybranych wartości: {0}.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Potwierdzono wybory.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Wybrane", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Naciśnij spację, aby posortować według tej kolumny", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Posortowane rosnąco.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Posortowane malejąco.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/pt.json b/prebuilt/react-native-sn-table/locale/locales/pt.json new file mode 100644 index 00000000..ce1ecbbc --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/pt.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Use as setas para navegar nas células da tabela e Tab para mover para os controles de paginação. Para toda a gama de navegação do teclado, consulte a documentação.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Mostrando {0} linhas e {1} colunas.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} de {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Ir para a primeira página", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Ir para a última página", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Ir para a próxima página", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "A página mudou. Mostrando página {0} de {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Ir para a página anterior", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Linhas por página", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Linhas por página mudou para {0}. Agora mostrando a primeira página.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Selecionar página", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "O valor está desmarcado.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "O valor está desmarcado. Modo de seleção encerrado.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "O valor não está selecionado.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Atualmente, há um valor selecionado.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "O valor está selecionado.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Atualmente, há {0} valores selecionados.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Seleções confirmadas.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Selecionado", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Pressione espaço para classificar por esta coluna", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Classificado em ordem crescente.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Classificado em ordem decrescente.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/ru.json b/prebuilt/react-native-sn-table/locale/locales/ru.json new file mode 100644 index 00000000..19160873 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/ru.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Используйте клавиши со стрелками для перемещения по ячейкам таблицы и клавишу табуляции для перехода к элементам управления разбивки на страницы. Полный перечень возможностей навигации с помощью клавиатуры см. в документации.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Показаны строки ({0}) и столбцы ({1}).", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} из {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Перейти к первой странице", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Перейти к последней странице", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Перейти к следующей странице", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "Страница изменилась. Показана страница {0} из {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Перейти к предыдущей странице", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Строк на странице", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Количество строк на странице изменилось на {0}. Теперь отображается первая страница.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Выберите страницу", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Выбор значения отменен.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Выбор значения отменен. Выполнен выход из режима выборки.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Значение не выбрано.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "В настоящее время выбрано одно значение.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Значение выбрано.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "В настоящее время выбраны значения: {0}.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Выборки подтверждены.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Выбрано", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Нажмите пробел, чтобы отсортировать по этому столбцу", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Сортировка в порядке возрастания.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Сортировка в порядке убывания.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/sv.json b/prebuilt/react-native-sn-table/locale/locales/sv.json new file mode 100644 index 00000000..e0ff589d --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/sv.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Använd piltangenterna för att navigera i tabellcellerna och tabbtangenten för att flytta till pagineringskontrollerna. Se dokumentationen för komplett information om tangentbordsnavigering.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "Visar {0} rader och {1} kolumner.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} av {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "Gå till första sidan", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Gå till sista sidan", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Gå till nästa sida", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "Sidan har ändrats. Visar sida {0} av {1}.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Gå till föregående sida", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Rader per sida", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Rader per sida har ändrats till {0}. Nu visas första sidan.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Välj sida", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Val av värde har tagits bort.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Val av värde har tagits bort. Lämnade urvalsläge.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Värdet är inte valt.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Just nu är ett värde valt.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Värdet är valt.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Just nu är {0} värden valda.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Valen har bekräftats.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Urval", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Tryck på Blanksteg om du vill sortera efter den här kolumnen", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Sorterat i stigande ordning.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Sorterat i fallande ordning.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/tr.json b/prebuilt/react-native-sn-table/locale/locales/tr.json new file mode 100644 index 00000000..300f55b0 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/tr.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "Tablo hücrelerinde gezinmek için ok tuşlarını, sayfalandırma kontrollerine geçmek için sekmeyi kullanın. Tam kapsamlı klavye gezintisi için belgelere bakın.", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "{0} satır ve {1} sütun gösteriliyor.", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} / {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "İlk sayfaya git", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "Son sayfaya git", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "Sonraki sayfaya git", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "Sayfa değişti. Sayfa {0} / {1} gösteriliyor.", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "Önceki sayfaya git", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "Sayfa başına satır sayısı", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "Sayfa başına satır sayısı {0} olarak değiştirildi. Şimdi ilk sayfa gösteriliyor.", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "Sayfa seç", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "Değer seçimi iptal edildi.", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "Değer seçimi iptal edildi. Seçim modundan çıkıldı.", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "Değer seçilmedi.", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "Şu anda seçili bir değer var.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "Değer seçildi.", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "Şu anda seçili {0} değer var.", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "Seçimler onaylandı.", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "Seçildi", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "Bu sütunda sıralama yapmak için boşluk tuşuna basın.", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "Artan düzende sıralanmıştır.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "Azalan düzende sıralanmıştır.", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/zh-CN.json b/prebuilt/react-native-sn-table/locale/locales/zh-CN.json new file mode 100644 index 00000000..edaf7dcb --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/zh-CN.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "使用箭头键在表格单元格中导航,使用 tab 键移动到分页控件。有关键盘导航的完整范围,请参阅文档。", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "正在显示 {0} 行,{1} 列。", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} / {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "转到第一页", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "转到最后一页", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "转到下一页", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "页面已更改。显示页面 {0} / {1}。", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "转到前一页", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "每页行数", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "每页行数已更改为 {0}。现在显示第一页。", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "选择页面", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "值已取消选择。", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "值已取消选择。已退出选择模式。", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "未选择值。", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "当前有一个已选择的值。", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "已选择值。", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "当前有 {0} 个已选择的值。", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "选择已确认。", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "已选择", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "按下空格键以在该列上排序。", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "已按升序排列。", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "已按降序排列。", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/locales/zh-TW.json b/prebuilt/react-native-sn-table/locale/locales/zh-TW.json new file mode 100644 index 00000000..23125240 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/locales/zh-TW.json @@ -0,0 +1,112 @@ +{ + "SNTable.Accessibility.NavigationInstructions": { + "value": "使用方向鍵以在表格儲存格中導覽,並使用 Tab 以前往分頁控制。如需完整的鍵盤導覽,請參閱文件。", + "comment": "Instructions read by screen reader when first focusing inside the table. (tew 211130)", + "version": "rON9K1cUvIGtROZHqXNfbg==" + }, + "SNTable.Accessibility.RowsAndColumns": { + "value": "顯示 {0} 列和 {1} 欄。", + "comment": "number of rows and columns announced by the screen reader. (tew 210929)", + "version": "HKjzXNhL9hPs/kFY0XyjTQ==" + }, + "SNTable.Pagination.DisplayedRowsLabel": { + "value": "{0} / {1}", + "comment": "label for the displayed rows, e.g. '1-100 of 234' (tew 211004)", + "version": "aQJbyf9x9LTIhITOr45yXA==" + }, + "SNTable.Pagination.FirstPage": { + "value": "移至第一頁", + "comment": "tooltip and aria-label for the first page button. (tew 220125)", + "version": "02xtL9RTBrrFoJ6sKxknMg==" + }, + "SNTable.Pagination.LastPage": { + "value": "移至最後一頁", + "comment": "tooltip and aria-label for the last page button. (tew 220125)", + "version": "r2oxhoCXSRbLzLA5mLygUg==" + }, + "SNTable.Pagination.NextPage": { + "value": "移至下一頁", + "comment": "tooltip and aria-label for the next page button. (tew 220125)", + "version": "zcTT0KquBNduHyvU9hnhoA==" + }, + "SNTable.Pagination.PageStatusReport": { + "value": "頁面已變更。顯示第 {0} 頁,共 {1} 頁。", + "comment": "Read via screenreader when the page has changed. {0} is replaced with the current page number. {1} is replaced with the total page number. (tew 211122)", + "version": "AOFznLoJupPIAwS7SchRcA==" + }, + "SNTable.Pagination.PreviousPage": { + "value": "移至上一頁", + "comment": "tooltip and aria-label for the previous page button. (tew 220125)", + "version": "0HFoHIB1i+ZETBO3YI76/g==" + }, + "SNTable.Pagination.RowsPerPage": { + "value": "每頁列數", + "comment": "label for the rows per page dropdown. (tew 210929)", + "version": "zuWpMmMSoOG/QRsKoVx3OA==" + }, + "SNTable.Pagination.RowsPerPageChange": { + "value": "每頁列數已變更為 {0}。現在顯示第一頁。", + "comment": "Read via screenreader when the number rows per page changes. {0} is replaced with the number of rows per page. (tew 211122)", + "version": "pMOIMgdSzJmSIj7NLXkS6g==" + }, + "SNTable.Pagination.SelectPage": { + "value": "選取頁面", + "comment": "Label for the dropdown to select a page.", + "version": "306b2wiIN019EPnVvTBSjA==" + }, + "SNTable.SelectionLabel.DeselectedValue": { + "value": "值已取消選取。", + "comment": "announced by screen readers when a value is deselected. (tew 210929)", + "version": "76E/5nke7HRRMEcF1fQbrA==" + }, + "SNTable.SelectionLabel.ExitedSelectionMode": { + "value": "值已取消選取。輸入的選取模式。", + "comment": "Announced by screen readers when deselecting and no values are selecting, causing the the chart to exit selection mode. (tew 210929)", + "version": "A4NCF1GIsCVwVg0MjPHUqw==" + }, + "SNTable.SelectionLabel.NotSelectedValue": { + "value": "未選取值。", + "comment": "announced by screen readers when focus is on an unselected value. (tew 210929)", + "version": "oj/qLPcfANXpS2oVVUzTdQ==" + }, + "SNTable.SelectionLabel.OneSelectedValue": { + "value": "目前有一個選取的值。", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "5llYYux+meuJItZE6XJhcQ==" + }, + "SNTable.SelectionLabel.SelectedValue": { + "value": "已選取值。", + "comment": "announced by screen readers when a value is selected. (tew 210929)", + "version": "ZJO4WnSdc7Wk5mDyEj4hug==" + }, + "SNTable.SelectionLabel.SelectedValues": { + "value": "目前有 {0} 個選取的值。", + "comment": "the amount of selected values in a dimension, announced by screen readers. (tew 210929)", + "version": "58lPVnscmLNuN+pvnPhkYA==" + }, + "SNTable.SelectionLabel.SelectionsConfirmed": { + "value": "選項已確認。", + "comment": "announced by screen readers when selections are confirmed. (tew 210929)", + "version": "g2xbBOIYPMUM0bFIQp5hDg==" + }, + "SNTable.SelectionLabel.selected": { + "value": "已選取", + "comment": "announced by screen readers for a selected value. (tew 210929)", + "version": "57xiDyOBQ0x8VcdHGd5tzg==" + }, + "SNTable.SortLabel.PressSpaceToSort": { + "value": "在此欄按下空格鍵以進行排序", + "comment": "instructions for sorting, announced by screen readers. (tew 210929)", + "version": "Qqrq+G7uktBDlaLsccl7ng==" + }, + "SNTable.SortLabel.SortedAscending": { + "value": "已依遞增順序排序。", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "LrRndbCXiDS4JMC+VCnW4A==" + }, + "SNTable.SortLabel.SortedDescending": { + "value": "已依遞減順序排序。", + "comment": "direction of sorting for a column, announced by screen readers. (tew 210929)", + "version": "81l9u+cBWloC+4R4s9akDQ==" + } +} diff --git a/prebuilt/react-native-sn-table/locale/scripts/generate-all.mjs b/prebuilt/react-native-sn-table/locale/scripts/generate-all.mjs new file mode 100755 index 00000000..961b9ef7 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/scripts/generate-all.mjs @@ -0,0 +1,55 @@ +#! /usr/bin/env node +import { globbySync } from 'globby'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import path from 'path'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +const LOCALES_DIR = path.resolve(dirname, '../locales'); +const LOCALES_FILES = globbySync([`${LOCALES_DIR}/*.json`]); +const LOCALE_PKG_DIR = path.resolve(dirname, '..'); +const ALL = path.resolve(`${LOCALE_PKG_DIR}`, 'all.json'); + +const LOCALES = { + 'en-US': 'en-US', + en: 'en-US', + de: 'de-DE', + fr: 'fr-FR', + it: 'it-IT', + ja: 'ja-JP', + ko: 'ko-KR', + nl: 'nl-NL', + pl: 'pl-PL', + pt: 'pt-BR', + ru: 'ru-RU', + sv: 'sv-SE', + tr: 'tr-TR', + 'zh-CN': 'zh-CN', + 'zh-TW': 'zh-TW', + es: 'es-ES', +}; + +const merged = {}; + +LOCALES_FILES.forEach((file) => { + const short = path.parse(file).name; + const locale = LOCALES[short]; + const content = JSON.parse(fs.readFileSync(file, 'utf8')); + + Object.keys(content).reduce((acc, curr) => { + const key = curr.replace(/\./g, '_'); + if (!acc[key]) { + acc[key] = { + id: curr, + }; + } + if (!acc[key].locale) { + acc[key].locale = {}; + } + acc[key].locale[locale] = content[curr].value; + return acc; + }, merged); +}); + +fs.writeFileSync(ALL, JSON.stringify(merged, ' ', 2)); diff --git a/prebuilt/react-native-sn-table/locale/src/index.js b/prebuilt/react-native-sn-table/locale/src/index.js new file mode 100644 index 00000000..e175d85c --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/src/index.js @@ -0,0 +1,19 @@ +// eslint-disable-next-line import/no-unresolved +import all from '../all.json'; + +export default function registerLocale(translator) { + if (translator && translator.get && translator.add) { + const t = 'SNTable.Accessibility.RowsAndColumns'; + const g = translator.get(t); + + // if the translated string is different from its id, + // the translations are assumed to already exist for the current locale + if (g !== t) { + return; + } + + Object.keys(all).forEach((key) => { + translator.add(all[key]); + }); + } +} diff --git a/prebuilt/react-native-sn-table/locale/tools/verify-translations.js b/prebuilt/react-native-sn-table/locale/tools/verify-translations.js new file mode 100644 index 00000000..c73a2c75 --- /dev/null +++ b/prebuilt/react-native-sn-table/locale/tools/verify-translations.js @@ -0,0 +1,34 @@ +// eslint-disable-next-line import/no-unresolved +const vars = require('../all.json'); + +const languages = [ + 'en-US', + 'it-IT', + 'zh-CN', + 'zh-TW', + 'ko-KR', + 'de-DE', + 'sv-SE', + 'es-ES', + 'pt-BR', + 'ja-JP', + 'fr-FR', + 'nl-NL', + 'tr-TR', + 'pl-PL', + 'ru-RU', +]; + +Object.keys(vars).forEach((key) => { + const supportLanguagesForString = Object.keys(vars[key].locale); + if (supportLanguagesForString.indexOf('en-US') === -1) { + // en-US must exist + throw new Error(`String '${vars[key].id}' is missing value for 'en-US'`); + } + for (let i = 0; i < languages.length; i++) { + if (supportLanguagesForString.indexOf(languages[i]) === -1) { + // eslint-disable-next-line no-console + console.warn(`String '${vars[key].id}' is missing value for '${languages[i]}'`); + } + } +}); diff --git a/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-announce-and-translations.spec.js b/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-announce-and-translations.spec.js new file mode 100644 index 00000000..76290486 --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-announce-and-translations.spec.js @@ -0,0 +1,95 @@ +import { announcementFactory } from '../use-announce-and-translations'; + +describe('announcementFactory', () => { + let rootElement; + let translator; + let announcer; + let announcerElement01; + let announcerElement02; + let previousAnnouncementElement; + let junkChar; + + beforeEach(() => { + announcerElement01 = global.document.createElement('div'); + announcerElement01.setAttribute('id', '#sn-table-announcer--01'); + announcerElement02 = global.document.createElement('div'); + announcerElement02.setAttribute('id', '#sn-table-announcer--02'); + + rootElement = { + querySelector: (query) => { + if (query === '#sn-table-announcer--01') return announcerElement01; + if (query === '#sn-table-announcer--02') return announcerElement02; + return announcerElement01; + }, + }; + translator = { get: (key) => key }; + previousAnnouncementElement = null; + junkChar = ' ­'; + }); + + it('should render a simple key', () => { + announcer = announcementFactory(rootElement, translator); + const key = ['SOME_SIMPLE_KEY']; + announcer({ keys: key }); + + expect(announcerElement01.innerHTML).toBe(`${key}${junkChar}`); + expect(announcerElement02.innerHTML).toHaveLength(0); + }); + + it('should render live element with proper attributes', () => { + announcer = announcementFactory(rootElement, translator); + const keys = ['SOME_SIMPLE_KEY']; + announcer({ keys, shouldBeAtomic: true, politeness: 'assertive' }); + + expect(announcerElement01.innerHTML).toBe(`${keys}${junkChar}`); + expect(announcerElement01.getAttribute('aria-atomic')).toBe('true'); + expect(announcerElement01.getAttribute('aria-live')).toBe('assertive'); + expect(announcerElement02.innerHTML).toHaveLength(0); + }); + + it('should render multiple keys', () => { + announcer = announcementFactory(rootElement, translator); + const keys = ['key#01', 'key#02']; + announcer({ keys }); + + expect(announcerElement01.innerHTML).toBe(`${keys.join(' ')}${junkChar}`); + expect(announcerElement02.innerHTML).toHaveLength(0); + }); + + it('should render multiple keys with arguments', () => { + announcer = announcementFactory(rootElement, translator); + const keys = ['key#01', ['key#02', 1, 2]]; + announcer({ keys }); + + expect(announcerElement01.innerHTML).toBe(`key#01 key#02${junkChar}`); + expect(announcerElement02.innerHTML).toHaveLength(0); + }); + + it('should render the junk char in odd function run iterations', () => { + announcer = announcementFactory(rootElement, translator); + const keys = ['key#01']; + announcer({ keys }); + + expect(announcerElement01.innerHTML).toBe(`${keys[0]}${junkChar}`); // extra space for the junk char + expect(announcerElement02.innerHTML).toHaveLength(0); + }); + + it('should be able to handle the concurrent announcement', () => { + const keys = ['key#01']; + previousAnnouncementElement = 'first-announcer-element'; + announcer = announcementFactory(rootElement, translator, previousAnnouncementElement); + announcer({ keys }); + + expect(announcerElement02.innerHTML).toBe(`${keys[0]}${junkChar}`); + }); + + it('should remove junkChar if the current announce element has it', () => { + const keys = ['key#01']; + announcerElement01.innerHTML = `${keys[0]}${junkChar}`; + announcer = announcementFactory(rootElement, translator); + announcer({ keys }); + + expect(announcerElement01.innerHTML).toBe(keys[0]); + expect(announcerElement02.innerHTML).toHaveLength(0); + }); +}); diff --git a/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-extended-theme.spec.js b/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-extended-theme.spec.js new file mode 100644 index 00000000..cbdb944e --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-extended-theme.spec.js @@ -0,0 +1,284 @@ +import { tableThemeColors } from '../use-extended-theme'; + +describe('tableThemeColors', () => { + let themeObjectBackgroundColor; + let themeTableBackgroundColor; + let rootElement; + const theme = { + getStyle: (base, path, attribute) => + attribute === 'backgroundColor' ? themeObjectBackgroundColor : themeTableBackgroundColor, + }; + let valueWithLightBackgroundColor = { + tableBackgroundColorFromTheme: 'inherit', + backgroundColor: undefined, + isBackgroundDarkColor: false, + isBackgroundTransparentColor: false, + body: { borderColor: '#D9D9D9' }, + borderColor: '#D9D9D9', + pagination: { + borderColor: '#D9D9D9', + color: '#404040', + iconColor: 'rgba(0, 0, 0, 0.54)', + disabledIconColor: 'rgba(0, 0, 0, 0.3)', + }, + }; + let valueWithDarkBackgroundColor = { + ...valueWithLightBackgroundColor, + isBackgroundDarkColor: true, + body: { borderColor: ' #F2F2F2' }, + borderColor: ' #F2F2F2', + pagination: { + borderColor: ' #F2F2F2', + color: 'rgba(255, 255, 255, 0.9)', + disabledIconColor: 'rgba(255, 255, 255, 0.3)', + iconColor: 'rgba(255, 255, 255, 0.9)', + }, + }; + + beforeEach(() => { + themeTableBackgroundColor = undefined; + themeObjectBackgroundColor = undefined; + }); + + describe('mashup', () => { + rootElement = { + closest: () => null, + }; + + describe('when there is no background color in the theme file', () => { + it('should return the valueWithLightBackgroundColor', () => { + const result = tableThemeColors(theme); + expect(result).toEqual(valueWithLightBackgroundColor); + }); + }); + + describe('when there is a background color in the theme file', () => { + describe('when this is only a object background color', () => { + describe('when the background color is opaque', () => { + it('should return the valueWithLightBackgroundColor when the background color is light', () => { + themeObjectBackgroundColor = '#fff'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ ...valueWithLightBackgroundColor, backgroundColor: themeObjectBackgroundColor }); + }); + + it('should return the valueWithDarkBackgroundColor when the background color is dark', () => { + themeObjectBackgroundColor = '#000'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ ...valueWithDarkBackgroundColor, backgroundColor: themeObjectBackgroundColor }); + }); + }); + + describe('when the background color is transparent', () => { + it('should return the valueWithLightBackgroundColor and isBackgroundTransparentColor to be true when the background color is light', () => { + themeObjectBackgroundColor = 'rgba(255, 255, 255, 0)'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: themeObjectBackgroundColor, + isBackgroundTransparentColor: true, + }); + }); + + it('should return the valueWithLightBackgroundColor when the background color is dark', () => { + themeObjectBackgroundColor = 'rgba(0, 0, 0, 0)'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: themeObjectBackgroundColor, + isBackgroundTransparentColor: true, + }); + }); + }); + }); + + describe('when this is only a table background color', () => { + it('should return the valueWithLightBackgroundColor, backgroundColor, and tableBackgroundColorFromTheme when the background color is light', () => { + themeObjectBackgroundColor = undefined; + themeTableBackgroundColor = '#fff'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: themeTableBackgroundColor, + tableBackgroundColorFromTheme: themeTableBackgroundColor, + }); + }); + + it('should return the valueWithDarkBackgroundColor, backgroundColor, and tableBackgroundColorFromTheme when the background color is dark', () => { + themeObjectBackgroundColor = undefined; + themeTableBackgroundColor = '#000'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithDarkBackgroundColor, + backgroundColor: themeTableBackgroundColor, + tableBackgroundColorFromTheme: themeTableBackgroundColor, + }); + }); + }); + + describe('when this are both object and table background color', () => { + it('should return the default value when the table background color is light', () => { + themeObjectBackgroundColor = '#000'; + themeTableBackgroundColor = '#fff'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: themeTableBackgroundColor, + tableBackgroundColorFromTheme: themeTableBackgroundColor, + }); + }); + + it('should return the valueWithDarkBackgroundColor when the table background color is dark', () => { + themeObjectBackgroundColor = '#fff'; + themeTableBackgroundColor = '#000'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithDarkBackgroundColor, + backgroundColor: themeTableBackgroundColor, + tableBackgroundColorFromTheme: themeTableBackgroundColor, + }); + }); + }); + }); + }); + + describe('client', () => { + rootElement = { + closest: (selector) => selector, + }; + let qvPanelSheetBackgroundColor; + let qvInnerObjectBackgroundColor; + global.window.getComputedStyle = (selector) => ({ + backgroundColor: selector === '.qv-panel-sheet' ? qvPanelSheetBackgroundColor : qvInnerObjectBackgroundColor, + }); + + beforeEach(() => { + valueWithLightBackgroundColor = { + ...valueWithLightBackgroundColor, + backgroundColor: 'rgba(0, 0, 0, 0)', + isBackgroundTransparentColor: true, + }; + valueWithDarkBackgroundColor = { + ...valueWithDarkBackgroundColor, + backgroundColor: 'rgba(0, 0, 0, 0)', + isBackgroundTransparentColor: true, + }; + qvPanelSheetBackgroundColor = '#fff'; + qvInnerObjectBackgroundColor = 'rgba(0, 0, 0, 0)'; + }); + + describe('when there is no background color from theme or css file', () => { + it('should return the valueWithLightBackgroundColor', () => { + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual(valueWithLightBackgroundColor); + }); + }); + + describe('when there is a background color from css file on sheet', () => { + describe('when the background color is dark', () => { + it('should return the valueWithDarkBackgroundColor', () => { + qvPanelSheetBackgroundColor = '#000'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual(valueWithDarkBackgroundColor); + }); + }); + }); + + describe('when there is a background color from theme file on object', () => { + describe('when the background color is opaque', () => { + describe('when the background color is light', () => { + it('should return the valueWithLightBackgroundColor, backgroundColor, and isBackgroundTransparentColor to be false', () => { + qvPanelSheetBackgroundColor = '#000'; + qvInnerObjectBackgroundColor = '#fff'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: qvInnerObjectBackgroundColor, + isBackgroundTransparentColor: false, + }); + }); + }); + + describe('when the background color is dark', () => { + it('should return the valueWithDarkBackgroundColor, backgroundColor, and isBackgroundTransparentColor to be false', () => { + qvInnerObjectBackgroundColor = '#000'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithDarkBackgroundColor, + backgroundColor: qvInnerObjectBackgroundColor, + isBackgroundTransparentColor: false, + }); + }); + }); + }); + + describe('when the background color is transparent', () => { + it('should return the valueWithLightBackgroundColor and backgroundColor', () => { + qvPanelSheetBackgroundColor = '#fff'; + qvInnerObjectBackgroundColor = 'rgba(255, 255, 255, 0)'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: qvInnerObjectBackgroundColor, + }); + }); + }); + }); + + describe('when there is a background color from theme file on table', () => { + describe('when the background color is opaque', () => { + describe('when the background color is light', () => { + it('should return the valueWithLightBackgroundColor and backgroundColor', () => { + themeTableBackgroundColor = '#fff'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: themeTableBackgroundColor, + tableBackgroundColorFromTheme: themeTableBackgroundColor, + isBackgroundTransparentColor: false, + }); + }); + }); + + describe('when the background color is dark', () => { + it('should return the valueWithLightBackgroundColor, backgroundColor, tableBackgroundColorFromTheme and isBackgroundTransparentColor to false', () => { + themeTableBackgroundColor = '#000'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithDarkBackgroundColor, + backgroundColor: themeTableBackgroundColor, + tableBackgroundColorFromTheme: themeTableBackgroundColor, + isBackgroundTransparentColor: false, + }); + }); + }); + }); + + describe('when the background color is transparent', () => { + it('should return the valueWithLightBackgroundColor, backgroundColor, tableBackgroundColorFromTheme', () => { + themeTableBackgroundColor = 'rgba(255, 255, 255, 0)'; + + const result = tableThemeColors(theme, rootElement); + expect(result).toEqual({ + ...valueWithLightBackgroundColor, + backgroundColor: themeTableBackgroundColor, + tableBackgroundColorFromTheme: themeTableBackgroundColor, + }); + }); + }); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-sorting.spec.js b/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-sorting.spec.js new file mode 100644 index 00000000..bb03f95b --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/__tests__/use-sorting.spec.js @@ -0,0 +1,77 @@ +import { sortingFactory } from '../use-sorting'; +import { generateLayout } from '../../__test__/generate-test-data'; + +describe('use-sorting', () => { + describe('sortingFactory', () => { + it('should return undefined when model is undefined', async () => { + const model = undefined; + const changeSortOrder = sortingFactory(model); + expect(changeSortOrder).toBeUndefined(); + }); + }); + + describe('changeSortOrder', () => { + let originalOrder; + let column; + let layout; + let model; + let changeSortOrder; + let expectedPatches; + + beforeEach(() => { + originalOrder = [0, 1, 2, 3]; + layout = generateLayout(2, 2, 2); + column = { isDim: true, dataColIdx: 1 }; + model = { + applyPatches: jest.fn(), + getEffectiveProperties: async () => + Promise.resolve({ + qHyperCubeDef: { + qInterColumnSortOrder: originalOrder, + }, + }), + }; + changeSortOrder = sortingFactory(model); + expectedPatches = [ + { + qPath: '/qHyperCubeDef/qInterColumnSortOrder', + qOp: 'replace', + qValue: '[0,1,2,3]', + }, + ]; + }); + + it('should call apply patches with second dimension first in sort order', async () => { + expectedPatches[0].qValue = '[1,0,2,3]'; + + await changeSortOrder(layout, column); + expect(model.applyPatches).toHaveBeenCalledWith(expectedPatches, true); + }); + + it('should call apply patches with another patch for qReverseSort for dimension', async () => { + column.dataColIdx = 0; + expectedPatches.push({ + qPath: '/qHyperCubeDef/qDimensions/0/qDef/qReverseSort', + qOp: 'replace', + qValue: 'true', + }); + + await changeSortOrder(layout, column); + expect(model.applyPatches).toHaveBeenCalledWith(expectedPatches, true); + }); + + it('should call apply patches with another patch for qReverseSort for measure', async () => { + column = { isDim: false, dataColIdx: 2 }; + originalOrder = [2, 0, 1, 3]; + expectedPatches[0].qValue = '[2,0,1,3]'; + expectedPatches.push({ + qPath: '/qHyperCubeDef/qMeasures/0/qDef/qReverseSort', + qOp: 'replace', + qValue: 'true', + }); + + await changeSortOrder(layout, column); + expect(model.applyPatches).toHaveBeenCalledWith(expectedPatches, true); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/nebula-hooks/use-announce-and-translations.ts b/prebuilt/react-native-sn-table/nebula-hooks/use-announce-and-translations.ts new file mode 100644 index 00000000..e1374069 --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/use-announce-and-translations.ts @@ -0,0 +1,58 @@ +import { stardust, useState, useEffect } from '@nebula.js/stardust'; +import registerLocale from '../locale/src'; +import { AnnounceArgs } from '../types'; + +// eslint-disable-next-line no-shadow +enum AnnouncerElements { + FIRST = 'first-announcer-element', + SECOND = 'second-announcer-element', +} + +/* creates the function for announcement */ +export const announcementFactory = (rootElement: Element, translator: stardust.Translator, prevAnnounceEl?: string) => { + let previousAnnouncementElement = prevAnnounceEl || null; + + /* updates the aria-live elements using the translation keys, makes sure it is announced every time it is called */ + return ({ keys, shouldBeAtomic = true, politeness = 'polite' }: AnnounceArgs) => { + const notation = keys + .map((key) => { + if (Array.isArray(key)) { + const [actualKey, ...rest] = key; + return translator.get(actualKey, rest); + } + return translator.get(key); + }) + .join(' '); + + const announceElement01 = rootElement.querySelector('#sn-table-announcer--01') as Element; + const announceElement02 = rootElement.querySelector('#sn-table-announcer--02') as Element; + + let announceElement: Element; + if (previousAnnouncementElement === AnnouncerElements.FIRST) { + announceElement = announceElement02; + previousAnnouncementElement = AnnouncerElements.SECOND; + } else { + announceElement = announceElement01; + previousAnnouncementElement = AnnouncerElements.FIRST; + } + + announceElement.innerHTML = announceElement.innerHTML.endsWith(` ­`) ? notation : `${notation} ­`; + announceElement.setAttribute('aria-atomic', shouldBeAtomic.toString()); + announceElement.setAttribute('aria-live', politeness); + }; +}; + +const useAnnounceAndTranslations = (rootElement: Element, translator: stardust.Translator) => { + const [announce, setAnnounce] = useState void)>(undefined); + + useEffect(() => { + if (rootElement && translator) { + registerLocale(translator); + setAnnounce(() => announcementFactory(rootElement, translator)); + } + }, [rootElement, translator.language()]); + + return announce; +}; + +export default useAnnounceAndTranslations; diff --git a/prebuilt/react-native-sn-table/nebula-hooks/use-extended-theme.js b/prebuilt/react-native-sn-table/nebula-hooks/use-extended-theme.js new file mode 100644 index 00000000..6d42cbbe --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/use-extended-theme.js @@ -0,0 +1,52 @@ +import { useMemo, useTheme } from '@nebula.js/stardust'; +import { isDarkColor, isTransparentColor } from '../table/utils/color-utils'; + +export const tableThemeColors = (theme, rootElement) => { + const qvInnerObject = rootElement?.closest('.qv-object .qv-inner-object'); + const objectBackgroundColorFromCSS = qvInnerObject && window.getComputedStyle(qvInnerObject).backgroundColor; + + const qvPanelSheet = rootElement?.closest('.qv-panel-sheet'); + const sheetBackgroundColorFromCSS = qvPanelSheet && window.getComputedStyle(qvPanelSheet).backgroundColor; + + const tableBackgroundColorFromTheme = theme.getStyle('', '', 'object.straightTable.backgroundColor'); + + const backgroundColorFromTheme = theme.getStyle('object', 'straightTable', 'backgroundColor'); + const backgroundColor = tableBackgroundColorFromTheme || objectBackgroundColorFromCSS || backgroundColorFromTheme; + const isBackgroundTransparentColor = isTransparentColor(backgroundColor); + const isBackgroundDarkColor = isDarkColor( + isBackgroundTransparentColor ? sheetBackgroundColorFromCSS : backgroundColor + ); + + const BORDER_COLOR = isBackgroundDarkColor ? ' #F2F2F2' : '#D9D9D9'; + + const borderColor = BORDER_COLOR; + const body = { borderColor: BORDER_COLOR }; + const pagination = { + borderColor: BORDER_COLOR, + color: isBackgroundDarkColor ? 'rgba(255, 255, 255, 0.9)' : '#404040', + iconColor: isBackgroundDarkColor ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.54)', + disabledIconColor: isBackgroundDarkColor ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)', + }; + + return { + tableBackgroundColorFromTheme: tableBackgroundColorFromTheme || 'inherit', + backgroundColor, + isBackgroundTransparentColor, + isBackgroundDarkColor, + borderColor, + body, + pagination, + }; +}; + +const useExtendedTheme = (rootElement) => { + const nebulaTheme = useTheme(); + const theme = useMemo( + () => ({ ...nebulaTheme, table: tableThemeColors(nebulaTheme, rootElement) }), + [nebulaTheme.name(), rootElement] + ); + + return theme; +}; + +export default useExtendedTheme; diff --git a/prebuilt/react-native-sn-table/nebula-hooks/use-extended-theme.native.js b/prebuilt/react-native-sn-table/nebula-hooks/use-extended-theme.native.js new file mode 100644 index 00000000..ee5fcbd9 --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/use-extended-theme.native.js @@ -0,0 +1,8 @@ +import { useTheme } from '@nebula.js/stardust'; + +const useExtendedTheme = () => { + const nebulaTheme = useTheme(); + return nebulaTheme; +}; + +export default useExtendedTheme; diff --git a/prebuilt/react-native-sn-table/nebula-hooks/use-react-root.js b/prebuilt/react-native-sn-table/nebula-hooks/use-react-root.js new file mode 100644 index 00000000..049d54f5 --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/use-react-root.js @@ -0,0 +1,18 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useEffect, useState } from '@nebula.js/stardust'; +import { createRoot } from 'react-dom/client'; + +import { mount } from '../table/Root'; + +export default function useReactRoot(rootElement) { + const [reactRoot, setReactRoot] = useState(); + + useEffect(() => { + if (rootElement) { + setReactRoot(createRoot(rootElement)); + mount(rootElement); + } + }, [rootElement]); + + return reactRoot; +} diff --git a/prebuilt/react-native-sn-table/nebula-hooks/use-react-root.native.js b/prebuilt/react-native-sn-table/nebula-hooks/use-react-root.native.js new file mode 100644 index 00000000..749d6867 --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/use-react-root.native.js @@ -0,0 +1,11 @@ +import { useEffect } from '@nebula.js/stardust'; +import { mount } from '../table/Root'; + +export default function useReactRoot(element) { + useEffect(() => { + if (element) { + mount(element); + } + }, [element]); + return element; +} diff --git a/prebuilt/react-native-sn-table/nebula-hooks/use-sorting.js b/prebuilt/react-native-sn-table/nebula-hooks/use-sorting.js new file mode 100644 index 00000000..d410a987 --- /dev/null +++ b/prebuilt/react-native-sn-table/nebula-hooks/use-sorting.js @@ -0,0 +1,50 @@ +import { useMemo } from '@nebula.js/stardust'; + +export const sortingFactory = (model) => { + if (!model) return undefined; + + return async (layout, column) => { + const { isDim, dataColIdx } = column; + // The sort order from the properties is needed since it contains hidden columns + const properties = await model.getEffectiveProperties(); + const sortOrder = properties.qHyperCubeDef.qInterColumnSortOrder; + const topSortIdx = sortOrder[0]; + + if (dataColIdx !== topSortIdx) { + sortOrder.splice(sortOrder.indexOf(dataColIdx), 1); + sortOrder.unshift(dataColIdx); + } + + const patches = [ + { + qPath: '/qHyperCubeDef/qInterColumnSortOrder', + qOp: 'replace', + qValue: `[${sortOrder.join(',')}]`, + }, + ]; + + // reverse + if (dataColIdx === topSortIdx) { + const { qDimensionInfo, qMeasureInfo } = layout.qHyperCube; + const idx = isDim ? dataColIdx : dataColIdx - qDimensionInfo.length; + const { qReverseSort } = isDim ? qDimensionInfo[idx] : qMeasureInfo[idx]; + const qPath = `/qHyperCubeDef/${isDim ? 'qDimensions' : 'qMeasures'}/${idx}/qDef/qReverseSort`; + + patches.push({ + qPath, + qOp: 'replace', + qValue: (!qReverseSort).toString(), + }); + } + + model.applyPatches(patches, true); + }; +}; + +const useSorting = (model) => { + const changeSortOrder = useMemo(() => sortingFactory(model), [model]); + + return changeSortOrder; +}; + +export default useSorting; diff --git a/prebuilt/react-native-sn-table/object-properties.js b/prebuilt/react-native-sn-table/object-properties.js new file mode 100644 index 00000000..4346089f --- /dev/null +++ b/prebuilt/react-native-sn-table/object-properties.js @@ -0,0 +1,168 @@ +/** + * @extends {GenericObjectProperties} + * @entry + */ +const properties = { + /** + * Current version of this generic object definition + * @type {string} + * @default + */ + version: process.env.PACKAGE_VERSION, + /** + * Extends HyperCubeDef, see Engine API: HyperCubeDef + * @extends {HyperCubeDef} + */ + qHyperCubeDef: { + /** + * The maximum amount of dimensions is 1000 + * @type {DimensionProperties[]} + */ + qDimensions: [], + /** + * The maximum amount of measures is 1000 + * @type {MeasureProperties[]} + */ + qMeasures: [], + /** @type {schemasNxHypercubeMode} */ + qMode: 'S', + /** @type {boolean} */ + qSuppressZero: false, + /** @type {boolean} */ + qSuppressMissing: true, + /** @type {number[]} */ + qColumnOrder: [], + /** @type {number[]} */ + columnWidths: [], + }, + /** + * Show title for the visualization + * @type {boolean=} + */ + showTitles: true, + /** + * Visualization title + * @type {(string|StringExpression)=} + */ + title: '', + /** + * Visualization subtitle + * @type {(string|StringExpression)=} + */ + subtitle: '', + /** + * Visualization footnote + * @type {(string|StringExpression)=} + */ + footnote: '', + /** + * totals settings + * @type {object} + */ + totals: { + /** + * Determines if the way totals row is showing is handle automatically, if `true` the `position` prop will be ignored + * @type {boolean=} + */ + show: true, + /** + * The position of the totals row, hiding it if set to `noTotals` + * @type {('top'|'bottom'|'noTotals')=} + */ + position: 'noTotals', + /** + * The label of the totals row, shown in the leftmost column + * @type {string=} + */ + label: 'Totals', + }, + /** + * Holds general styling + * @type {Styling[]} + */ + components: [], + + disableLasso: true, +}; + +/** + * Extends `NxDimension`, see Engine API: `NxDimension` + * @typedef {object} DimensionProperties + * @extends NxDimension + * @property {InlineDimensionDef} qDef + * @property {AttributeExpressionProperties[]} qAttributeExpressions + */ + +/** + * Extends `NxMeasure`, see Engine API: `NxMeasure` + * @typedef {object} MeasureProperties + * @extends NxMeasure + * @property {NxInlineMeasureDef} qDef + * @property {AttributeExpressionProperties[]} qAttributeExpressions + */ + +/** + * Extends `NxInlineDimensionDef`, see Engine API: `NxInlineDimensionDef`. + * @typedef {object} InlineDimensionDef + * @extends NxInlineDimensionDef + * @property {TextAlign=} textAlign + */ + +/** + * Extends `NxInlineMeasureDef`, see Engine API: `NxInlineMeasureDef`. + * @typedef {object} InlineMeasureDef + * @extends NxInlineMeasureDef + * @property {TextAlign=} textAlign + */ + +/** + * Extends `NxAttrExprDef`, see Engine API: `NxAttrExprDef`. + * Column specific styling overrides general styling, that is defined in `components`. + * @typedef {object} AttributeExpressionProperties + * @extends NxAttrExprDef - expression resolving into a valid color + * @property {('cellForegroundColor'|'cellBackgroundColor')} id - specifying what the color applies to + */ + +/** + * Holds text alignment for a specific column. + * @typedef {object} TextAlign + * @extends NxInlineDimensionDef + * @property {boolean} auto - If true, sets the alignment based on the type of column (left for dimension, right for measure) + * @property {('left'|'center'|'right')} align - Is used (and mandatory) if `auto` is false + */ + +/** + * General styling for all columns. + * Split up into header and content (body) styling. + * If any property is not set, default values specific for each property is used. + * @typedef {object} Styling + * @property {string} key - This should be set to `theme` + * @property {ContentStyling=} content + * @property {HeaderStyling=} header + */ + +/** + * Holds properties for font size, font color and hover styling. + * @typedef {object} ContentStyling + * @property {number=} fontSize - Defaults to `14` + * @property {PaletteColor=} fontColor - Defaults to `#404040` + * @property {boolean=} hoverEffect - Toggles hover effect + * @property {PaletteColor=} hoverColor - Background hover color. Uses `#f4f4f4` if no hover colors are set, is transparent if only `hoverFontColor` is set + * @property {PaletteColor=} hoverFontColor - When only `hoverColor` is set, this is adjusted to either `#f4f4f4` or `#ffffff` for optimal contrast + */ + +/** + * Holds properties for font size and color. + * @typedef {object} HeaderStyling + * @property {number=} fontSize - Defaults to `14` + * @property {PaletteColor=} fontColor - Defaults to `#404040` + */ + +/** + * Color information structure. Holds the actual color and index in palette + * @typedef {object} PaletteColor + * @property {string} color - Color as hex string (mandatory if index: -1) + * @property {number} index - Index in palette + */ + +export default properties; diff --git a/prebuilt/react-native-sn-table/package.json b/prebuilt/react-native-sn-table/package.json new file mode 100644 index 00000000..d38d63b1 --- /dev/null +++ b/prebuilt/react-native-sn-table/package.json @@ -0,0 +1,33 @@ +{ + "name": "@nebula.js/react-native-sn-table", + "version": "1.14.121", + "description": "table supernova", + "license": "MIT", + "author": "QlikTech International AB", + "keywords": [ + "qlik", + "nebula", + "stardust" + ], + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://github.com/qlik-oss/sn-table/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/qlik-oss/sn-table.git" + }, + "engines": { + "node": ">=16" + }, + "main": "index.js", + "module": "core/esm/index.js", + "peerDependencies": { + "@nebula.js/stardust": "2.x || ^3.0.0-alpha" + }, + "resolutions": { + "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest" + } +} \ No newline at end of file diff --git a/prebuilt/react-native-sn-table/table/Root.jsx b/prebuilt/react-native-sn-table/table/Root.jsx new file mode 100644 index 00000000..c05909b2 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/Root.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { StyleSheetManager } from 'styled-components'; +import { ThemeProvider } from '@mui/material/styles'; +import rtlPluginSc from 'stylis-plugin-rtl-sc'; +import TableWrapper from './components/TableWrapper'; +import { TableContextProvider } from './context'; +import muiSetup from './mui-setup'; + +export function render(reactRoot, props) { + const { direction, selectionsAPI } = props; + const muiTheme = muiSetup(direction); + + reactRoot.render( + + + + + + + + ); +} + +export function teardown(reactRoot) { + reactRoot.unmount(); +} + +export function mount() { + /* noop in web */ +} diff --git a/prebuilt/react-native-sn-table/table/Root.native.jsx b/prebuilt/react-native-sn-table/table/Root.native.jsx new file mode 100644 index 00000000..0d63e73c --- /dev/null +++ b/prebuilt/react-native-sn-table/table/Root.native.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import Table from './native/Table'; + +export function mount(rootElement) { + rootElement.mount((props) => ); +} + +export function render(rootElement, data) { + rootElement.renderComponent(data); +} + +export function teardown() {} diff --git a/prebuilt/react-native-sn-table/table/components/AnnounceElements.jsx b/prebuilt/react-native-sn-table/table/components/AnnounceElements.jsx new file mode 100644 index 00000000..741eaacf --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/AnnounceElements.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { TableAnnouncer } from '../styles'; + +const AnnounceElements = () => { + return ( + <> + + + + ); +}; + +export default AnnounceElements; diff --git a/prebuilt/react-native-sn-table/table/components/FooterWrapper.jsx b/prebuilt/react-native-sn-table/table/components/FooterWrapper.jsx new file mode 100644 index 00000000..6438500e --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/FooterWrapper.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { StyledFooterWrapper } from '../styles'; + +export default function FooterWrapper({ children, theme, footerContainer }) { + return footerContainer ? ( + ReactDOM.createPortal(children, footerContainer) + ) : ( + {children} + ); +} + +FooterWrapper.defaultProps = { + footerContainer: null, +}; + +FooterWrapper.propTypes = { + theme: PropTypes.object.isRequired, + children: PropTypes.any.isRequired, + footerContainer: PropTypes.object, +}; diff --git a/prebuilt/react-native-sn-table/table/components/PaginationContent.jsx b/prebuilt/react-native-sn-table/table/components/PaginationContent.jsx new file mode 100644 index 00000000..416052dc --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/PaginationContent.jsx @@ -0,0 +1,188 @@ +import React, { memo } from 'react'; +import PropTypes from 'prop-types'; +import InputLabel from '@mui/material/InputLabel'; +import FormControl from '@mui/material/FormControl'; +import Box from '@mui/material/Box'; +import FirstPageIcon from '@mui/icons-material/FirstPage'; +import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'; +import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; +import LastPageIcon from '@mui/icons-material/LastPage'; + +import { StyledSelect, StyledIconButton } from '../styles'; +import { handleLastTab } from '../utils/handle-key-press'; + +const icons = { + FirstPage: FirstPageIcon, + PreviousPage: KeyboardArrowLeft, + NextPage: KeyboardArrowRight, + LastPage: LastPageIcon, + FirstPageRTL: LastPageIcon, + PreviousPageRTL: KeyboardArrowRight, + NextPageRTL: KeyboardArrowLeft, + LastPageRTL: FirstPageIcon, +}; + +export const shouldShow = (component, width) => { + switch (component) { + case 'selectPage': + return width > 700; + case 'rppOptions': + return width > 550; + case 'firstLast': + return width > 350; + case 'displayedRows': + return width > 250; + default: + return false; + } +}; + +function PaginationContent({ + theme, + direction, + tableData, + pageInfo, + setPageInfo, + keyboard, + translator, + constraints, + footerContainer, + isSelectionMode, + rect, + handleChangePage, + announce, +}) { + const { totalRowCount, totalColumnCount, totalPages, paginationNeeded } = tableData; + const { page, rowsPerPage, rowsPerPageOptions } = pageInfo; + + if (!paginationNeeded) return null; + + const paginationTheme = theme.table.pagination; + const onFirstPage = page === 0; + const onLastPage = page >= totalPages - 1; + // The elements can be focused in sequential keyboard navigation: + // - When nebula handles keyboard navigation + // and focus is somewhere inside the extension, or + // - When nebula does not handle keyboard navigation + const tabIndex = !keyboard.enabled || keyboard.active ? 0 : -1; + const width = footerContainer ? footerContainer.getBoundingClientRect().width : rect.width; + const showFirstAndLast = shouldShow('firstLast', width); + const showRowsPerPage = !isSelectionMode && shouldShow('rppOptions', width) && totalColumnCount <= 100; + const displayedRowsText = translator.get('SNTable.Pagination.DisplayedRowsLabel', [ + `${page * rowsPerPage + 1} - ${Math.min((page + 1) * rowsPerPage, totalRowCount)}`, + totalRowCount, + ]); + + const handleChangeRowsPerPage = (evt) => { + setPageInfo({ ...pageInfo, page: 0, rowsPerPage: +evt.target.value }); + announce({ keys: [['SNTable.Pagination.RowsPerPageChange', evt.target.value]], politeness: 'assertive' }); + }; + + const handleSelectPage = (event) => handleChangePage(+event.target.value); + + const handleLastButtonTab = keyboard.enabled ? (event) => handleLastTab(event, isSelectionMode, keyboard) : null; + + const getButton = (disabledCondition, pageNumber, type, onKeyDown = null) => { + const iconType = `${type}${direction === 'rtl' ? 'RTL' : ''}`; + const IconComponent = icons[iconType]; + + return ( + handleChangePage(pageNumber) : null} + aria-disabled={disabledCondition} + aria-label={translator.get(`SNTable.Pagination.${type}`)} + title={!constraints.passive ? translator.get(`SNTable.Pagination.${type}`) : undefined} + tabIndex={tabIndex} + onKeyDown={onKeyDown} + > + + + ); + }; + + const getDropdown = (name, value, options, handleChange) => { + const translationName = `SNTable.Pagination.${name}`; + const id = `${name}-dropdown`; + const inputProps = { + tabIndex, + id, + 'data-testid': id, + style: { color: paginationTheme.color, height: 30 }, + }; + + return ( + + + {`${translator.get(translationName)}:`} + + + {options} + + + ); + }; + + const rppOptions = ( + <> + {rowsPerPageOptions.map((opt) => ( + + ))} + + ); + + const pageOptions = ( + <> + {[...Array(totalPages).keys()].map((pageIdx, index) => ( + + ))} + + ); + + return ( + <> + {showRowsPerPage && getDropdown('RowsPerPage', rowsPerPage, rppOptions, handleChangeRowsPerPage)} + {shouldShow('displayedRows', width) && {displayedRowsText}} + {shouldShow('selectPage', width) && getDropdown('SelectPage', page, pageOptions, handleSelectPage)} + {showFirstAndLast && getButton(onFirstPage, 0, 'FirstPage')} + {getButton(onFirstPage, page - 1, 'PreviousPage')} + {getButton(onLastPage, page + 1, 'NextPage', !showFirstAndLast ? handleLastButtonTab : null)} + {showFirstAndLast && getButton(onLastPage, totalPages - 1, 'LastPage', handleLastButtonTab)} + + ); +} + +PaginationContent.defaultProps = { + direction: null, + footerContainer: null, +}; + +PaginationContent.propTypes = { + theme: PropTypes.object.isRequired, + tableData: PropTypes.object.isRequired, + pageInfo: PropTypes.object.isRequired, + setPageInfo: PropTypes.func.isRequired, + keyboard: PropTypes.object.isRequired, + translator: PropTypes.object.isRequired, + constraints: PropTypes.object.isRequired, + isSelectionMode: PropTypes.bool.isRequired, + rect: PropTypes.object.isRequired, + handleChangePage: PropTypes.func.isRequired, + announce: PropTypes.func.isRequired, + direction: PropTypes.string, + footerContainer: PropTypes.object, +}; + +export default memo(PaginationContent); diff --git a/prebuilt/react-native-sn-table/table/components/TableBodyWrapper.jsx b/prebuilt/react-native-sn-table/table/components/TableBodyWrapper.jsx new file mode 100644 index 00000000..f8ebd0d8 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/TableBodyWrapper.jsx @@ -0,0 +1,122 @@ +import React, { useEffect, useMemo, memo } from 'react'; +import PropTypes from 'prop-types'; +import getCellRenderer from '../utils/get-cell-renderer'; +import { useContextSelector, TableContext } from '../context'; +import { StyledTableBody, StyledBodyRow } from '../styles'; +import { addSelectionListeners } from '../utils/selections-utils'; +import { getBodyCellStyle } from '../utils/styling-utils'; +import { bodyHandleKeyPress, bodyHandleKeyUp } from '../utils/handle-key-press'; +import { handleClickToFocusBody } from '../utils/handle-accessibility'; + +function TableBodyWrapper({ + rootElement, + tableData, + constraints, + selectionsAPI, + layout, + theme, + setShouldRefocus, + keyboard, + tableWrapperRef, + announce, + children, +}) { + const { rows, columns, paginationNeeded, totalsPosition } = tableData; + const columnsStylingInfoJSON = JSON.stringify(columns.map((column) => column.stylingInfo)); + const setFocusedCellCoord = useContextSelector(TableContext, (value) => value.setFocusedCellCoord); + const selectionDispatch = useContextSelector(TableContext, (value) => value.selectionDispatch); + // constraints.active: true - turn off interactions that affect the state of the visual + // representation including selection, zoom, scroll, etc. + // constraints.select: true - turn off selections. + const isSelectionsEnabled = !constraints.active && !constraints.select; + const columnRenderers = useMemo( + () => + JSON.parse(columnsStylingInfoJSON).map((stylingInfo) => + getCellRenderer(!!stylingInfo.length, isSelectionsEnabled) + ), + [columnsStylingInfoJSON, isSelectionsEnabled] + ); + const bodyCellStyle = useMemo(() => getBodyCellStyle(layout, theme), [layout, theme]); + const hoverEffect = layout.components?.[0]?.content?.hoverEffect; + const cellStyle = { color: bodyCellStyle.color, backgroundColor: theme.table.backgroundColor }; + + useEffect(() => { + addSelectionListeners({ api: selectionsAPI, selectionDispatch, setShouldRefocus, keyboard, tableWrapperRef }); + }, []); + + return ( + + {totalsPosition === 'top' ? children : undefined} + {rows.map((row) => ( + + {columns.map((column, columnIndex) => { + const { id, align } = column; + const cell = row[id]; + const CellRenderer = columnRenderers[columnIndex]; + const handleKeyDown = (evt) => { + bodyHandleKeyPress({ + evt, + rootElement, + selectionsAPI, + cell, + selectionDispatch, + isSelectionsEnabled, + setFocusedCellCoord, + announce, + keyboard, + paginationNeeded, + totalsPosition, + }); + }; + + return ( + CellRenderer && ( + bodyHandleKeyUp(evt, selectionDispatch)} + onMouseDown={() => + handleClickToFocusBody(cell, rootElement, setFocusedCellCoord, keyboard, totalsPosition) + } + > + {cell.qText} + + ) + ); + })} + + ))} + {totalsPosition === 'bottom' ? children : undefined} + + ); +} + +TableBodyWrapper.propTypes = { + rootElement: PropTypes.object.isRequired, + tableData: PropTypes.object.isRequired, + constraints: PropTypes.object.isRequired, + selectionsAPI: PropTypes.object.isRequired, + layout: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + setShouldRefocus: PropTypes.func.isRequired, + keyboard: PropTypes.object.isRequired, + tableWrapperRef: PropTypes.object.isRequired, + announce: PropTypes.func.isRequired, + children: PropTypes.object.isRequired, +}; + +export default memo(TableBodyWrapper); diff --git a/prebuilt/react-native-sn-table/table/components/TableHeadWrapper.jsx b/prebuilt/react-native-sn-table/table/components/TableHeadWrapper.jsx new file mode 100644 index 00000000..6eb1fe17 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/TableHeadWrapper.jsx @@ -0,0 +1,103 @@ +import React, { memo, useEffect, useMemo, useRef } from 'react'; +import PropTypes from 'prop-types'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; + +import { useContextSelector, TableContext } from '../context'; +import { VisuallyHidden, StyledHeadRow, StyledSortLabel } from '../styles'; +import { getHeaderStyle } from '../utils/styling-utils'; +import { headHandleKeyPress } from '../utils/handle-key-press'; +import { handleMouseDownLabelToFocusHeadCell, handleClickToFocusHead } from '../utils/handle-accessibility'; + +function TableHeadWrapper({ + rootElement, + tableData, + theme, + layout, + changeSortOrder, + constraints, + translator, + selectionsAPI, + keyboard, +}) { + const { columns, paginationNeeded } = tableData; + const setHeadRowHeight = useContextSelector(TableContext, (value) => value.setHeadRowHeight); + const isFocusInHead = useContextSelector(TableContext, (value) => value.focusedCellCoord[0] === 0); + const setFocusedCellCoord = useContextSelector(TableContext, (value) => value.setFocusedCellCoord); + const headerStyle = useMemo(() => getHeaderStyle(layout, theme), [layout, theme]); + const headRowRef = useRef(); + + useEffect(() => { + setHeadRowHeight(headRowRef.current.getBoundingClientRect().height); + }, [headRowRef.current, headerStyle.fontSize, headRowRef.current?.getBoundingClientRect().height]); + + return ( + + + {columns.map((column, columnIndex) => { + // The first cell in the head is focusable in sequential keyboard navigation, + // when nebula does not handle keyboard navigation + const tabIndex = columnIndex === 0 && !keyboard.enabled ? 0 : -1; + const isCurrentColumnActive = layout.qHyperCube.qEffectiveInterColumnSortOrder[0] === column.dataColIdx; + const handleKeyDown = (evt) => { + headHandleKeyPress({ + evt, + rootElement, + cellCoord: [0, columnIndex], + column, + changeSortOrder, + layout, + isSortingEnabled: !constraints.active, + setFocusedCellCoord, + }); + }; + + return ( + handleClickToFocusHead(columnIndex, rootElement, setFocusedCellCoord, keyboard)} + onClick={() => !selectionsAPI.isModal() && !constraints.active && changeSortOrder(layout, column)} + > + handleMouseDownLabelToFocusHeadCell(evt, rootElement, columnIndex)} + > + {column.label} + {isFocusInHead && ( + + {translator.get('SNTable.SortLabel.PressSpaceToSort')} + + )} + + + ); + })} + + + ); +} + +TableHeadWrapper.propTypes = { + rootElement: PropTypes.object.isRequired, + tableData: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + layout: PropTypes.object.isRequired, + changeSortOrder: PropTypes.func.isRequired, + constraints: PropTypes.object.isRequired, + translator: PropTypes.object.isRequired, + selectionsAPI: PropTypes.object.isRequired, + keyboard: PropTypes.object.isRequired, +}; + +export default memo(TableHeadWrapper); diff --git a/prebuilt/react-native-sn-table/table/components/TableTotals.jsx b/prebuilt/react-native-sn-table/table/components/TableTotals.jsx new file mode 100644 index 00000000..b8f65f37 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/TableTotals.jsx @@ -0,0 +1,52 @@ +import React, { memo, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { useContextSelector, TableContext } from '../context'; +import { getTotalsCellStyle } from '../utils/styling-utils'; +import { totalHandleKeyPress } from '../utils/handle-key-press'; +import { removeAndFocus } from '../utils/handle-accessibility'; +import { StyledHeadRow, StyledTotalsCell } from '../styles'; + +function TableTotals({ rootElement, tableData, theme, layout, keyboard }) { + const { columns, paginationNeeded, totalsPosition, rows } = tableData; + const headRowHeight = useContextSelector(TableContext, (value) => value.headRowHeight); + const setFocusedCellCoord = useContextSelector(TableContext, (value) => value.setFocusedCellCoord); + const totalsStyle = useMemo(() => getTotalsCellStyle(layout, theme), [layout, theme.name()]); + const isTop = totalsPosition === 'top'; + + return ( + + {columns.map((column, columnIndex) => { + const cellCoord = [isTop ? 1 : rows.length + 1, columnIndex]; + return ( + { + totalHandleKeyPress(e, rootElement, cellCoord, setFocusedCellCoord); + }} + onMouseDown={() => { + removeAndFocus(cellCoord, rootElement, setFocusedCellCoord, keyboard); + }} + > + {column.totalInfo} + + ); + })} + + ); +} + +TableTotals.propTypes = { + rootElement: PropTypes.object.isRequired, + tableData: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + layout: PropTypes.object.isRequired, + keyboard: PropTypes.object.isRequired, +}; + +export default memo(TableTotals); diff --git a/prebuilt/react-native-sn-table/table/components/TableWrapper.jsx b/prebuilt/react-native-sn-table/table/components/TableWrapper.jsx new file mode 100644 index 00000000..50297fcb --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/TableWrapper.jsx @@ -0,0 +1,159 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useCallback } from 'react'; +import Table from '@mui/material/Table'; + +import AnnounceElements from './AnnounceElements'; +import TableBodyWrapper from './TableBodyWrapper'; +import TableHeadWrapper from './TableHeadWrapper'; +import TableTotals from './TableTotals'; +import FooterWrapper from './FooterWrapper'; +import { useContextSelector, TableContext } from '../context'; +import { StyledTableContainer, StyledTableWrapper } from '../styles'; + +import PaginationContent from './PaginationContent'; +import useDidUpdateEffect from '../hooks/use-did-update-effect'; +import useFocusListener from '../hooks/use-focus-listener'; +import useScrollListener from '../hooks/use-scroll-listener'; +import { handleTableWrapperKeyDown } from '../utils/handle-key-press'; +import { updateFocus, handleResetFocus, getCellElement } from '../utils/handle-accessibility'; +import { handleNavigateTop } from '../utils/handle-scroll'; + +export default function TableWrapper(props) { + const { + rootElement, + tableData, + pageInfo, + setPageInfo, + constraints, + translator, + selectionsAPI, + theme, + keyboard, + direction, + footerContainer, + announce, + } = props; + const { totalColumnCount, totalRowCount, totalPages, paginationNeeded, rows, columns } = tableData; + const { page, rowsPerPage } = pageInfo; + const isSelectionMode = selectionsAPI.isModal(); + const focusedCellCoord = useContextSelector(TableContext, (value) => value.focusedCellCoord); + const setFocusedCellCoord = useContextSelector(TableContext, (value) => value.setFocusedCellCoord); + const shouldRefocus = useRef(false); + const tableContainerRef = useRef(); + const tableWrapperRef = useRef(); + + const setShouldRefocus = useCallback(() => { + shouldRefocus.current = rootElement.getElementsByTagName('table')[0].contains(document.activeElement); + }, [rootElement]); + + const handleChangePage = useCallback( + (pageIdx) => { + setPageInfo({ ...pageInfo, page: pageIdx }); + announce({ + keys: [['SNTable.Pagination.PageStatusReport', (pageIdx + 1).toString(), totalPages.toString()]], + politeness: 'assertive', + }); + }, + [pageInfo, setPageInfo, totalPages, announce] + ); + + const handleKeyDown = (evt) => { + handleTableWrapperKeyDown({ + evt, + totalRowCount, + page, + rowsPerPage, + handleChangePage, + setShouldRefocus, + keyboard, + isSelectionMode, + }); + }; + + useFocusListener(tableWrapperRef, shouldRefocus, keyboard); + useScrollListener(tableContainerRef, direction); + + useEffect( + () => handleNavigateTop({ tableContainerRef, focusedCellCoord, rootElement }), + [tableContainerRef, focusedCellCoord, rootElement] + ); + + useDidUpdateEffect(() => { + // When nebula handles keyboard navigation and keyboard.active changes, + // make sure to blur or focus the cell corresponding to focusedCellCoord + // when keyboard.focus() runs, keyboard.active is true + // when keyboard.blur() runs, keyboard.active is false + updateFocus({ focusType: keyboard.active ? 'focus' : 'blur', cell: getCellElement(rootElement, focusedCellCoord) }); + }, [keyboard.active]); + + // Except for first render, whenever the size of the data (number of rows per page, rows, columns) or page changes, + // reset tabindex to first cell. If some cell had focus, focus the first cell as well. + useDidUpdateEffect(() => { + handleResetFocus({ + focusedCellCoord, + rootElement, + shouldRefocus, + setFocusedCellCoord, + isSelectionMode, + keyboard, + announce, + }); + }, [rows.length, totalRowCount, totalColumnCount, page]); + + const tableAriaLabel = `${translator.get('SNTable.Accessibility.RowsAndColumns', [ + rows.length + 1, + columns.length, + ])} ${translator.get('SNTable.Accessibility.NavigationInstructions')}`; + + return ( + + + +
+ + + + +
+ + {!constraints.active && ( + + + + )} + + ); +} + +TableWrapper.defaultProps = { + direction: null, + footerContainer: null, +}; + +TableWrapper.propTypes = { + rootElement: PropTypes.object.isRequired, + tableData: PropTypes.object.isRequired, + pageInfo: PropTypes.object.isRequired, + setPageInfo: PropTypes.func.isRequired, + translator: PropTypes.object.isRequired, + constraints: PropTypes.object.isRequired, + selectionsAPI: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + keyboard: PropTypes.object.isRequired, + announce: PropTypes.func.isRequired, + footerContainer: PropTypes.object, + direction: PropTypes.string, +}; diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/AnnounceElements.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/AnnounceElements.spec.jsx new file mode 100644 index 00000000..a2a1c656 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/AnnounceElements.spec.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import AnnounceElements from '../AnnounceElements'; + +describe('', () => { + it('should render the Announce component properly', () => { + const result = render(); + const firstAnnounceElement = result.container.querySelector('#sn-table-announcer--01'); + const secondAnnounceElement = result.container.querySelector('#sn-table-announcer--02'); + + expect(firstAnnounceElement).toBeVisible(); + expect(secondAnnounceElement).toBeVisible(); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/PaginationContent.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/PaginationContent.spec.jsx new file mode 100644 index 00000000..9f909567 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/PaginationContent.spec.jsx @@ -0,0 +1,242 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; + +import PaginationContent from '../PaginationContent'; +import * as handleAccessibility from '../../utils/handle-accessibility'; + +describe('', () => { + let theme; + let direction; + let tableData; + let pageInfo; + let setPageInfo; + let titles; + let handleChangePage; + let rect; + let translator; + let isSelectionMode; + let keyboard; + let constraints; + let footerContainer; + let announce; + + const renderPagination = () => + render( + + ); + + beforeEach(() => { + theme = { + table: { pagination: { color: '', iconColor: '' } }, + }; + direction = 'ltr'; + titles = [ + 'SNTable.Pagination.FirstPage', + 'SNTable.Pagination.PreviousPage', + 'SNTable.Pagination.NextPage', + 'SNTable.Pagination.LastPage', + ]; + tableData = { + totalRowCount: 200, + totalColumnCount: 5, + paginationNeeded: true, + totalPages: 3, + }; + pageInfo = { + page: 0, + rowsPerPage: 25, + rowsPerPageOptions: [10, 25, 100], + }; + setPageInfo = jest.fn(); + handleChangePage = jest.fn(); + rect = { width: 750 }; + translator = { get: (s) => s }; + isSelectionMode = false; + keyboard = { enabled: true }; + constraints = {}; + announce = jest.fn(); + jest.spyOn(handleAccessibility, 'focusSelectionToolbar').mockImplementation(() => jest.fn()); + }); + + afterEach(() => jest.clearAllMocks()); + + describe('rendering', () => { + it('should return null when paginationNeeded is false', () => { + tableData.paginationNeeded = false; + const { queryByText } = renderPagination(); + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeNull(); + }); + + it('should render all sub-components', () => { + const { getAllByTestId, queryByTestId, queryByText } = renderPagination(); + const buttons = getAllByTestId('pagination-action-icon-button'); + + expect(buttons).toHaveLength(4); + expect(queryByTestId('SelectPage-dropdown')).toBeVisible(); + expect(queryByTestId('RowsPerPage-dropdown')).toBeVisible(); + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeVisible(); + }); + + it('should render all sub-components except page dropdown', () => { + rect.width = 600; + + const { getAllByTestId, queryByTestId, queryByText } = renderPagination(); + const buttons = getAllByTestId('pagination-action-icon-button'); + + expect(buttons).toHaveLength(4); + expect(queryByTestId('SelectPage-dropdown')).toBeNull(); + expect(queryByTestId('RowsPerPage-dropdown')).toBeVisible(); + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeVisible(); + }); + + it('should render all sub-components except rpp/page dropdown', () => { + rect.width = 500; + + const { getAllByTestId, queryByTestId, queryByText } = renderPagination(); + const buttons = getAllByTestId('pagination-action-icon-button'); + + expect(buttons).toHaveLength(4); + expect(queryByTestId('SelectPage-dropdown')).toBeNull(); + expect(queryByTestId('RowsPerPage-dropdown')).toBeNull(); + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeVisible(); + }); + it('should only render previous/next buttons and current rows info', () => { + rect.width = 300; + + const { getAllByTestId, queryByTestId, queryByText } = renderPagination(); + const buttons = getAllByTestId('pagination-action-icon-button'); + + expect(buttons).toHaveLength(2); + expect(queryByTestId('SelectPage-dropdown')).toBeNull(); + expect(queryByTestId('RowsPerPage-dropdown')).toBeNull(); + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeVisible(); + }); + it('should only render previous/next buttons', () => { + rect.width = 200; + + const { getAllByTestId, queryByTestId, queryByText } = renderPagination(); + const buttons = getAllByTestId('pagination-action-icon-button'); + + expect(buttons).toHaveLength(2); + expect(queryByTestId('SelectPage-dropdown')).toBeNull(); + expect(queryByTestId('RowsPerPage-dropdown')).toBeNull(); + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeNull(); + }); + + it('should render all buttons in right order when left-to-right direction', () => { + const { getAllByTestId } = renderPagination(); + + const renderedNames = getAllByTestId('pagination-action-icon-button'); + renderedNames.forEach((nameNode, index) => { + expect(Object.values(nameNode)[1].title).toBe(titles[index]); + }); + }); + + it('should render all buttons in right order when right-to-left direction', () => { + direction = 'rtl'; + const { getAllByTestId } = renderPagination(); + + const renderedNames = getAllByTestId('pagination-action-icon-button'); + renderedNames.forEach((nameNode, index) => { + expect(Object.values(nameNode)[1].title).toBe(titles[index]); + }); + }); + }); + + describe('interaction', () => { + it('should call handleChangePage when clicking next page', () => { + const { queryByTitle } = renderPagination(); + + fireEvent.click(queryByTitle('SNTable.Pagination.NextPage')); + expect(handleChangePage).toHaveBeenCalledWith(1); + }); + + it('should call handleChangePage when clicking previous page', () => { + pageInfo.page = 1; + const { queryByTitle } = renderPagination(); + + fireEvent.click(queryByTitle('SNTable.Pagination.PreviousPage')); + expect(handleChangePage).toHaveBeenCalledWith(0); + }); + + it('should call handleChangePage when clicking last page', () => { + const { queryByTitle } = renderPagination(); + + fireEvent.click(queryByTitle('SNTable.Pagination.LastPage')); + expect(handleChangePage).toHaveBeenCalledWith(2); + }); + + it('should call handleChangePage when clicking first page', () => { + pageInfo.page = 2; + const { queryByTitle } = renderPagination(); + + fireEvent.click(queryByTitle('SNTable.Pagination.FirstPage')); + expect(handleChangePage).toHaveBeenCalledWith(0); + }); + + it('should not call focusSelectionToolbar when pressing tab on last page button and isSelectionMode is false', () => { + const { queryByTitle } = renderPagination(); + fireEvent.keyDown(queryByTitle('SNTable.Pagination.LastPage'), { key: 'Tab' }); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + }); + + it('should not call focusSelectionToolbar when pressing shift + tab on last page button and isSelectionMode is true', () => { + isSelectionMode = true; + + const { queryByTitle } = renderPagination(); + fireEvent.keyDown(queryByTitle('SNTable.Pagination.LastPage'), { key: 'Tab', shiftKey: true }); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + }); + + it('should call focusSelectionToolbar when pressing tab on last page button and isSelectionMode is true', () => { + isSelectionMode = true; + + const { queryByTitle } = renderPagination(); + fireEvent.keyDown(queryByTitle('SNTable.Pagination.LastPage'), { key: 'Tab' }); + expect(handleAccessibility.focusSelectionToolbar).toHaveBeenCalledTimes(1); + }); + + it('should call focusSelectionToolbar when pressing tab on next page button, isSelectionMode is true and tableWidth < 350', () => { + isSelectionMode = true; + rect.width = 300; + + const { queryByTitle } = renderPagination(); + fireEvent.keyDown(queryByTitle('SNTable.Pagination.NextPage'), { key: 'Tab' }); + expect(handleAccessibility.focusSelectionToolbar).toHaveBeenCalledTimes(1); + }); + + it('should call handleChangePage when selecting page from dropdown', () => { + pageInfo.page = 0; + const { queryByTestId } = renderPagination(); + + fireEvent.change(queryByTestId('SelectPage-dropdown'), { target: { value: 1 } }); + expect(handleChangePage).toHaveBeenCalledWith(1); + }); + + it('should call setPageInfo when selecting rows per page from dropdown', () => { + const targetRowsPerPage = 10; + const { queryByTestId } = renderPagination(); + + fireEvent.change(queryByTestId('RowsPerPage-dropdown'), { target: { value: targetRowsPerPage } }); + expect(setPageInfo).toHaveBeenCalledWith({ ...pageInfo, rowsPerPage: targetRowsPerPage }); + expect(announce).toHaveBeenCalledWith({ + keys: [['SNTable.Pagination.RowsPerPageChange', `${targetRowsPerPage}`]], + politeness: 'assertive', + }); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/TableBodyWrapper.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/TableBodyWrapper.spec.jsx new file mode 100644 index 00000000..a312c9da --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/TableBodyWrapper.spec.jsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { generateDataPages, generateLayout } from '../../../__test__/generate-test-data'; +import manageData from '../../../handle-data'; +import { TableContextProvider } from '../../context'; +import TableBodyWrapper from '../TableBodyWrapper'; +import * as selectionsUtils from '../../utils/selections-utils'; +import * as getCellRenderer from '../../utils/get-cell-renderer'; +import * as handleKeyPress from '../../utils/handle-key-press'; +import * as handleAccessibility from '../../utils/handle-accessibility'; + +describe('', () => { + const rootElement = {}; + const setShouldRefocus = () => {}; + const keyboard = {}; + const tableWrapperRef = {}; + const announce = () => {}; + const model = { getHyperCubeData: async () => generateDataPages(2, 2, 2) }; + + let tableData; + let constraints; + let selectionsAPI; + let layout; + let theme; + let cellRendererSpy; + + const renderTableBody = () => + render( + + + + ); + + beforeEach(async () => { + jest.spyOn(selectionsUtils, 'addSelectionListeners').mockImplementation(() => jest.fn()); + tableData = await manageData(model, generateLayout(1, 1, 2, [], [{ qText: '100' }]), { top: 0, height: 100 }); + constraints = {}; + selectionsAPI = { + isModal: () => true, + }; + theme = { + getColorPickerColor: () => {}, + name: () => {}, + getStyle: () => {}, + table: { body: { borderColor: '' } }, + }; + layout = {}; + cellRendererSpy = jest.fn(); + }); + + afterEach(() => jest.clearAllMocks()); + + it('should render 2x2 table body and call CellRenderer', () => { + jest.spyOn(getCellRenderer, 'default').mockImplementation(() => { + cellRendererSpy(); + return 'td'; + }); + + const { queryByText } = renderTableBody(); + + expect(cellRendererSpy).toHaveBeenCalledTimes(2); + expect(queryByText(tableData.rows[0]['col-0'].qText)).toBeVisible(); + expect(queryByText(tableData.rows[0]['col-1'].qText)).toBeVisible(); + expect(queryByText(tableData.rows[1]['col-0'].qText)).toBeVisible(); + expect(queryByText(tableData.rows[1]['col-1'].qText)).toBeVisible(); + }); + + it('should call bodyHandleKeyPress on key down', () => { + jest.spyOn(handleKeyPress, 'bodyHandleKeyPress').mockImplementation(() => jest.fn()); + + const { queryByText } = renderTableBody(); + fireEvent.keyDown(queryByText(tableData.rows[0]['col-0'].qText)); + + expect(handleKeyPress.bodyHandleKeyPress).toHaveBeenCalledTimes(1); + }); + + it('should call handleClickToFocusBody on mouseDown', () => { + jest.spyOn(handleAccessibility, 'handleClickToFocusBody').mockImplementation(() => jest.fn()); + + const { queryByText } = renderTableBody(); + fireEvent.mouseDown(queryByText(tableData.rows[0]['col-0'].qText)); + + expect(handleAccessibility.handleClickToFocusBody).toHaveBeenCalledTimes(1); + }); + + it('should call bodyHandleKeyUp on key up', () => { + jest.spyOn(handleKeyPress, 'bodyHandleKeyUp').mockImplementation(() => jest.fn()); + + const { queryByText } = renderTableBody(); + fireEvent.keyUp(queryByText(tableData.rows[0]['col-0'].qText)); + + expect(handleKeyPress.bodyHandleKeyUp).toHaveBeenCalledTimes(1); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/TableHeadWrapper.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/TableHeadWrapper.spec.jsx new file mode 100644 index 00000000..3797c22f --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/TableHeadWrapper.spec.jsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TableHeadWrapper from '../TableHeadWrapper'; +import { TableContextProvider } from '../../context'; +import * as handleKeyPress from '../../utils/handle-key-press'; +import * as handleAccessibility from '../../utils/handle-accessibility'; + +describe('', () => { + const rootElement = {}; + let tableData; + let theme; + let layout; + let changeSortOrder; + let constraints; + let selectionsAPI; + let keyboard; + let translator; + + const renderTableHead = (cellCoordMock) => + render( + + + + ); + + beforeEach(() => { + tableData = { + columns: [ + { id: 1, align: 'left', label: 'someDim', sortDirection: 'asc', isDim: true, dataColIdx: 0 }, + { id: 2, align: 'right', label: 'someMsr', sortDirection: 'desc', isDim: false, dataColIdx: 1 }, + ], + }; + theme = { + getColorPickerColor: () => {}, + name: () => {}, + getStyle: () => {}, + table: { body: { borderColor: '' } }, + }; + layout = { + qHyperCube: { + qEffectiveInterColumnSortOrder: [0, 1], + }, + }; + changeSortOrder = jest.fn(); + constraints = { + active: false, + }; + selectionsAPI = { + isModal: () => false, + }; + keyboard = { + enabled: false, + }; + translator = { get: (s) => s }; + }); + + it('should render table head', () => { + const { queryByText } = renderTableHead(); + + expect(queryByText(tableData.columns[0].label)).toBeVisible(); + expect(queryByText(tableData.columns[1].label)).toBeVisible(); + }); + + it('should call changeSortOrder when clicking a header cell', () => { + const { queryByText } = renderTableHead(); + fireEvent.click(queryByText(tableData.columns[0].label)); + + expect(changeSortOrder).toHaveBeenCalledWith(layout, tableData.columns[0]); + }); + + it('should not call changeSortOrder when clicking a header cell in edit mode', () => { + constraints = { + active: true, + }; + const { queryByText } = renderTableHead(); + fireEvent.click(queryByText(tableData.columns[0].label)); + + expect(changeSortOrder).not.toHaveBeenCalled(); + }); + + it('should not call changeSortOrder when clicking a header cell and cells are selected', () => { + selectionsAPI = { + isModal: () => true, + }; + const { queryByText } = renderTableHead(); + fireEvent.click(queryByText(tableData.columns[0].label)); + + expect(changeSortOrder).not.toHaveBeenCalled(); + }); + + it('should call headHandleKeyPress when keyDown on a header cell', () => { + jest.spyOn(handleKeyPress, 'headHandleKeyPress').mockImplementation(() => jest.fn()); + + const { queryByText } = renderTableHead(); + fireEvent.keyDown(queryByText(tableData.columns[0].label)); + + expect(handleKeyPress.headHandleKeyPress).toHaveBeenCalledTimes(1); + }); + + it('should call handleClickToFocusHead and handleMouseDownLabelToFocusHeadCell when clicking a header cell label', () => { + jest.spyOn(handleAccessibility, 'handleClickToFocusHead').mockImplementation(() => jest.fn()); + jest.spyOn(handleAccessibility, 'handleMouseDownLabelToFocusHeadCell').mockImplementation(() => jest.fn()); + + const { queryByText } = renderTableHead(); + fireEvent.mouseDown(queryByText(tableData.columns[0].label)); + + expect(handleAccessibility.handleClickToFocusHead).toHaveBeenCalledTimes(1); + expect(handleAccessibility.handleMouseDownLabelToFocusHeadCell).toHaveBeenCalledTimes(1); + }); + + it('should change `aria-pressed` and `aria-sort` when you sort by second column', () => { + tableData = { + columns: [ + { ...tableData.columns[0], sortDirection: 'desc' }, + { ...tableData.columns[1], sortDirection: 'asc' }, + ], + columnOrder: tableData.columnOrder, + }; + layout = { + qHyperCube: { + qEffectiveInterColumnSortOrder: [1, 0], + }, + }; + + const { queryByText } = renderTableHead(); + const firstColQuery = queryByText(tableData.columns[0].label).closest('th'); + const secondColQuery = queryByText(tableData.columns[1].label).closest('th'); + + expect(firstColQuery.getAttribute('aria-sort')).toBeNull(); + expect(firstColQuery.getAttribute('aria-pressed')).toBe('false'); + expect(secondColQuery.getAttribute('aria-sort')).toBe('ascending'); + expect(secondColQuery.getAttribute('aria-pressed')).toBe('true'); + }); + + it('should render the visually hidden text instead of `aria-label` and has correct `scope` properly', () => { + const { queryByText, queryByTestId } = renderTableHead(); + + // check scope + const tableColumn = queryByText(tableData.columns[0].label).closest('th'); + expect(tableColumn.getAttribute('scope')).toBe('col'); + + // check label + const tableColumnSortlabel = queryByTestId('VHL-for-col-0'); + expect(tableColumnSortlabel).toHaveTextContent('SNTable.SortLabel.PressSpaceToSort'); + }); + + it('should not render visually hidden text while you are out of table header', () => { + const { queryByTestId } = renderTableHead([1, 1]); + + const firstColHiddenLabel = queryByTestId('VHL-for-col-0'); + const secondColHiddenLabel = queryByTestId('VHL-for-col-1'); + + expect(firstColHiddenLabel).toBeNull(); + expect(secondColHiddenLabel).toBeNull(); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/TableTotals.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/TableTotals.spec.jsx new file mode 100644 index 00000000..21f1c819 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/TableTotals.spec.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TableTotals from '../TableTotals'; +import { TableContextProvider } from '../../context'; +import { getTotalPosition } from '../../../handle-data'; +import * as handleKeyPress from '../../utils/handle-key-press'; +import * as handleAccessibility from '../../utils/handle-accessibility'; +import { generateLayout } from '../../../__test__/generate-test-data'; + +describe('', () => { + const rootElement = {}; + let tableData; + let theme; + let layout; + let selectionsAPI; + let keyboard; + let translator; + + const renderTableTotals = (cellCoordMock) => + render( + + {getTotalPosition(layout) === 'top' && ( + + )} + + ); + + beforeEach(() => { + tableData = { + columns: [ + { id: 1, isDim: true, dataColIdx: 0, totalInfo: 'Totals' }, + { id: 2, isDim: false, dataColIdx: 1, totalInfo: '350' }, + ], + rows: ['rowOne', 'rowTwo'], + }; + theme = { + getColorPickerColor: () => {}, + name: () => {}, + getStyle: () => {}, + table: { body: { borderColor: '' } }, + }; + layout = generateLayout(2, 2, 10, [], [{ qText: '350' }, { qText: '-' }]); + keyboard = { + enabled: false, + }; + translator = { get: (s) => s }; + selectionsAPI = { + isModal: () => false, + }; + }); + + it('should show the total row when table total is rendered', () => { + layout.totals.position = 'top'; + const { queryByText } = renderTableTotals(); + expect(queryByText(tableData.columns[0].totalInfo)).toHaveTextContent('Totals'); + expect(queryByText(tableData.columns[1].totalInfo)).toHaveTextContent('350'); + }); + + describe('Handle keys call when TableTotals is rendered', () => { + beforeEach(() => { + layout.totals.position = 'top'; + }); + afterEach(() => jest.clearAllMocks()); + it('should call totalHandleKeyPress when keyDown on a total cell', () => { + jest.spyOn(handleKeyPress, 'totalHandleKeyPress').mockImplementation(() => jest.fn()); + const { queryByText } = renderTableTotals(); + fireEvent.keyDown(queryByText(tableData.columns[0].totalInfo)); + expect(handleKeyPress.totalHandleKeyPress).toHaveBeenCalledTimes(1); + }); + + it('should call removeAndFocus when clicking a total cell', () => { + jest.spyOn(handleAccessibility, 'removeAndFocus').mockImplementation(() => jest.fn()); + const { queryByText } = renderTableTotals(); + fireEvent.mouseDown(queryByText(tableData.columns[0].totalInfo)); + expect(handleAccessibility.removeAndFocus).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/TableWrapper.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/TableWrapper.spec.jsx new file mode 100644 index 00000000..3ffa3b20 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/TableWrapper.spec.jsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; + +import { TableContextProvider } from '../../context'; +import TableWrapper from '../TableWrapper'; +import TableBodyWrapper from '../TableBodyWrapper'; +import TableHeadWrapper from '../TableHeadWrapper'; +import * as handleKeyPress from '../../utils/handle-key-press'; +import * as handleScroll from '../../utils/handle-scroll'; + +describe('', () => { + let tableData; + let pageInfo; + let setPageInfo; + let constraints; + let selectionsAPI; + let modal; + let rootElement; + let keyboard; + let translator; + let rect; + let theme; + + const renderTableWrapper = () => + render( + + + + ); + + beforeEach(() => { + // When wrapping a component in memo, the actual functional component is stored on type + jest.spyOn(TableHeadWrapper, 'type').mockImplementation(() => ); + jest.spyOn(TableBodyWrapper, 'type').mockImplementation(() => ); + + tableData = { + totalRowCount: 200, + totalColumnCount: 10, + paginationNeeded: true, + rows: [{ qText: '1' }], + columns: [{}], + }; + pageInfo = { page: 0, rowsPerPage: 100, rowsPerPageOptions: [10, 25, 100] }; + setPageInfo = jest.fn(); + constraints = {}; + selectionsAPI = { + isModal: () => modal, + }; + modal = false; + rootElement = { + getElementsByClassName: () => [], + getElementsByTagName: () => [{ clientHeight: {}, contains: jest.fn() }], + querySelector: () => {}, + }; + keyboard = { enabled: false, active: false }; + translator = { get: (s) => s }; + rect = { + width: 750, + }; + theme = { + getStyle: () => {}, + table: { + body: { + borderColor: '', + }, + pagination: { + borderColor: '', + }, + }, + }; + }); + + afterEach(() => jest.clearAllMocks()); + + it('should render table with pagination', () => { + const { queryByLabelText, queryByText, queryByTestId } = renderTableWrapper(); + + expect( + queryByLabelText(`${'SNTable.Accessibility.RowsAndColumns'} ${'SNTable.Accessibility.NavigationInstructions'}`) + ).toBeVisible(); + expect(queryByTestId('table-container').getAttribute('tabindex')).toBe('-1'); + expect(queryByTestId('table-container').getAttribute('role')).toBe('application'); + // Just checking that the pagination has rendered, we do more thorough checking in the PaginationContent tests + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeVisible(); + }); + + it('should not render pagination when constraints.active is true', () => { + constraints.active = true; + const { queryByLabelText, queryByText, queryByTestId } = renderTableWrapper(); + + expect( + queryByLabelText(`${'SNTable.Accessibility.RowsAndColumns'} ${'SNTable.Accessibility.NavigationInstructions'}`) + ).toBeVisible(); + expect(queryByTestId('table-container').getAttribute('tabindex')).toBe('-1'); + expect(queryByTestId('table-container').getAttribute('role')).toBe('application'); + expect(queryByText('SNTable.Pagination.DisplayedRowsLabel')).toBeNull(); + }); + + it('should call handleTableWrapperKeyDown when press control key on the table', () => { + jest.spyOn(handleKeyPress, 'handleTableWrapperKeyDown').mockImplementation(() => jest.fn()); + const { queryByLabelText } = renderTableWrapper(); + + fireEvent.keyDown(queryByLabelText('SNTable.Pagination.RowsPerPage:'), { key: 'Control', code: 'ControlLeft' }); + expect(handleKeyPress.handleTableWrapperKeyDown).toHaveBeenCalledTimes(1); + }); + + it('should call handleHorizontalScroll when scroll on the table', () => { + jest.spyOn(handleScroll, 'handleHorizontalScroll').mockImplementation(() => jest.fn()); + const { queryByTestId } = renderTableWrapper(); + + fireEvent.wheel(queryByTestId('table-container'), { deltaX: 100 }); + expect(handleScroll.handleHorizontalScroll).toHaveBeenCalledTimes(1); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/withColumnStyling.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/withColumnStyling.spec.jsx new file mode 100644 index 00000000..d6ec3d99 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/withColumnStyling.spec.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import * as withColumnStyling from '../withColumnStyling'; +import * as stylingUtils from '../../utils/styling-utils'; + +describe('withColumnStyling', () => { + let HOC; + let cell; + let column; + let styling; + + beforeEach(() => { + HOC = withColumnStyling.default((props) =>
{props.children}
); + jest.spyOn(stylingUtils, 'getColumnStyle').mockImplementation(() => jest.fn()); + + styling = {}; + cell = { + qAttrExps: {}, + }; + column = { + stylingInfo: [], + }; + }); + + afterEach(() => jest.clearAllMocks()); + + it('should render table head', () => { + const { queryByText } = render( + + someValue + + ); + + expect(queryByText('someValue')).toBeVisible(); + expect(stylingUtils.getColumnStyle).toHaveBeenCalledWith(styling, cell.qAttrExps, column.stylingInfo); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/withSelections.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/withSelections.spec.jsx new file mode 100644 index 00000000..eead5d92 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/withSelections.spec.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; + +import { TableContextProvider } from '../../context'; +import * as withSelections from '../withSelections'; +import { SelectionActions } from '../../utils/selections-utils'; + +describe('withSelections', () => { + const selectionsAPI = { isModal: () => false }; + let HOC; + let value; + let cell; + let evt; + let styling; + let announce; + let theme; + let themeBackgroundColor; + let selectionDispatchMock; + + const renderWithSelections = () => + render( + + + + ); + + beforeEach(() => { + HOC = withSelections.default((props) =>
{props.value}
); + + value = '100'; + cell = { + isSelectable: true, + }; + evt = { button: 0 }; + styling = {}; + announce = jest.fn(); + theme = { + getStyle: () => {}, + }; + themeBackgroundColor = '#123456'; + selectionDispatchMock = jest.fn(); + }); + + afterEach(() => jest.clearAllMocks()); + + it('should render a mocked component with the passed value', () => { + const { queryByText } = renderWithSelections(); + + expect(queryByText(value)).toBeVisible(); + }); + + it('should call selectCell on mouseUp', () => { + const { queryByText } = renderWithSelections(); + fireEvent.mouseUp(queryByText(value)); + + expect(selectionDispatchMock).toHaveBeenCalledTimes(1); + expect(selectionDispatchMock).toHaveBeenCalledWith({ + type: SelectionActions.SELECT, + payload: { cell, evt: expect.anything(), announce }, + }); + }); + + it('should not call selectCell on mouseUp when measure', () => { + cell.isSelectable = false; + + const { queryByText } = renderWithSelections(); + fireEvent.mouseUp(queryByText(value)); + + expect(selectionDispatchMock).not.toHaveBeenCalled(); + }); + + it('should not call selectCell on mouseUp when right button', () => { + evt.button = 2; + + const { queryByText } = renderWithSelections(); + fireEvent.mouseUp(queryByText(value), evt); + + expect(selectionDispatchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/__tests__/withStyling.spec.jsx b/prebuilt/react-native-sn-table/table/components/__tests__/withStyling.spec.jsx new file mode 100644 index 00000000..a369fa11 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/__tests__/withStyling.spec.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import * as withStyling from '../withStyling'; + +describe('withStyling', () => { + let HOC; + let styling; + + beforeEach(() => { + HOC = withStyling.default((props) =>
{props.children}
); + + styling = {}; + }); + + it('should render table cell', () => { + const { queryByText } = render(someValue); + + expect(queryByText('someValue')).toBeVisible(); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/components/withColumnStyling.jsx b/prebuilt/react-native-sn-table/table/components/withColumnStyling.jsx new file mode 100644 index 00000000..38194d1b --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/withColumnStyling.jsx @@ -0,0 +1,24 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { getColumnStyle } from '../utils/styling-utils'; + +export default function withColumnStyling(CellComponent) { + const HOC = (props) => { + const { cell, column, styling, ...passThroughProps } = props; + + const columnStyling = useMemo( + () => getColumnStyle(styling, cell.qAttrExps, column.stylingInfo), + [styling, cell.qAttrExps, column.stylingInfo] + ); + + return ; + }; + + HOC.propTypes = { + cell: PropTypes.object.isRequired, + column: PropTypes.object.isRequired, + styling: PropTypes.object.isRequired, + }; + + return HOC; +} diff --git a/prebuilt/react-native-sn-table/table/components/withSelections.jsx b/prebuilt/react-native-sn-table/table/components/withSelections.jsx new file mode 100644 index 00000000..e9711e6e --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/withSelections.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useContextSelector, TableContext } from '../context'; +import { getSelectionStyle } from '../utils/styling-utils'; +import { getCellSelectionState, SelectionActions } from '../utils/selections-utils'; + +export default function withSelections(CellComponent) { + const HOC = (props) => { + const { cell, styling, announce, ...passThroughProps } = props; + const cellSelectionState = useContextSelector(TableContext, (value) => getCellSelectionState(cell, value)); + const selectionDispatch = useContextSelector(TableContext, (value) => value.selectionDispatch); + + const handleMouseUp = (evt) => + cell.isSelectable && + evt.button === 0 && + selectionDispatch({ type: SelectionActions.SELECT, payload: { cell, evt, announce } }); + + const selectionStyling = getSelectionStyle(styling, cellSelectionState); + + return ; + }; + + HOC.propTypes = { + styling: PropTypes.object.isRequired, + cell: PropTypes.object.isRequired, + announce: PropTypes.func.isRequired, + }; + + return HOC; +} diff --git a/prebuilt/react-native-sn-table/table/components/withStyling.jsx b/prebuilt/react-native-sn-table/table/components/withStyling.jsx new file mode 100644 index 00000000..424ea0ed --- /dev/null +++ b/prebuilt/react-native-sn-table/table/components/withStyling.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function withStyling(CellComponent) { + const HOC = (props) => { + const { + styling: { selectedCellClass, ...style }, + component, + align, + children, + scope, + tabIndex, + onKeyDown, + onMouseDown, + onKeyUp, + onMouseUp, + } = props; + + return ( + + {children} + + ); + }; + + HOC.defaultProps = { + component: null, + scope: null, + onMouseUp: null, + }; + + HOC.propTypes = { + styling: PropTypes.object.isRequired, + component: PropTypes.string, + align: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + scope: PropTypes.string, + tabIndex: PropTypes.number.isRequired, + onKeyDown: PropTypes.func.isRequired, + onKeyUp: PropTypes.func.isRequired, + onMouseDown: PropTypes.func.isRequired, + onMouseUp: PropTypes.func, + }; + + return HOC; +} diff --git a/prebuilt/react-native-sn-table/table/context/TableContext.tsx b/prebuilt/react-native-sn-table/table/context/TableContext.tsx new file mode 100644 index 00000000..0d865fd5 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/context/TableContext.tsx @@ -0,0 +1,60 @@ +import React, { useState, useReducer, createContext } from 'react'; +import { stardust } from '@nebula.js/stardust'; +import PropTypes from 'prop-types'; +import { createSelectorProvider } from './createSelectorProvider'; +import { reducer } from '../utils/selections-utils'; +import { ExtendedSelectionAPI } from '../../types'; + +export const TableContext = createContext({}); + +const ProviderWithSelector = createSelectorProvider(TableContext); + +interface ContextProviderProps { + children: JSX.Element; + selectionsAPI: stardust.ObjectSelections; + cellCoordMock: [number, number]; + selectionDispatchMock: jest.Mock; +} + +export const TableContextProvider = ({ + children, + selectionsAPI, + cellCoordMock, + selectionDispatchMock, +}: ContextProviderProps) => { + const [headRowHeight, setHeadRowHeight] = useState(); + const [focusedCellCoord, setFocusedCellCoord] = useState(cellCoordMock || [0, 0]); + const [selectionState, selectionDispatch] = useReducer(reducer, { + rows: {}, + colIdx: -1, + api: selectionsAPI as ExtendedSelectionAPI, // TODO: update nebula api with correct selection api type + isSelectMultiValues: false, + }); + + return ( + + {children} + + ); +}; + +TableContextProvider.defaultProps = { + cellCoordMock: undefined, + selectionDispatchMock: undefined, +}; + +TableContextProvider.propTypes = { + children: PropTypes.any.isRequired, + selectionsAPI: PropTypes.object.isRequired, + cellCoordMock: PropTypes.arrayOf(PropTypes.number), + selectionDispatchMock: PropTypes.func, +}; diff --git a/prebuilt/react-native-sn-table/table/context/createSelectorProvider.tsx b/prebuilt/react-native-sn-table/table/context/createSelectorProvider.tsx new file mode 100644 index 00000000..4ab12722 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/context/createSelectorProvider.tsx @@ -0,0 +1,60 @@ +import React, { + Context, + createContext, + FunctionComponent, + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; + +const contextMap: Map, Context> = new Map(); + +interface ProviderProps { + readonly value: T; + readonly children: ReactNode; +} + +type ContextAccessor = () => T | undefined; +type ContextListener = (state: T) => void; +type ContextListeners = Set>; +export type SelectorContextType = readonly [ContextAccessor, ContextListeners]; + +export function createSelectorProvider(OriginalContext: Context): FunctionComponent> { + const SelectorContext = createContext>([() => undefined, new Set()]); + + contextMap.set(OriginalContext, SelectorContext); + + const Provider = ({ children, value }: ProviderProps) => { + const contextValueRef = useRef(value); + + const listeners = useRef>(new Set()); + + useEffect(() => { + contextValueRef.current = value; + + listeners.current.forEach((listener) => { + listener(value); + }); + }, [value]); + + const getContextValue: ContextAccessor = useCallback(() => { + return contextValueRef.current; + }, [contextValueRef]); + + const contextValue: SelectorContextType = useMemo(() => [getContextValue, listeners.current], [contextValueRef]); + + return ( + + {children} + + ); + }; + + return Provider; +} + +export function getSelectorContext(context: Context): Context> | undefined { + return contextMap.get(context); +} diff --git a/prebuilt/react-native-sn-table/table/context/index.ts b/prebuilt/react-native-sn-table/table/context/index.ts new file mode 100644 index 00000000..f09fec1e --- /dev/null +++ b/prebuilt/react-native-sn-table/table/context/index.ts @@ -0,0 +1,2 @@ +export { useContextSelector } from './useContextSelector'; +export { TableContext, TableContextProvider } from './TableContext'; diff --git a/prebuilt/react-native-sn-table/table/context/useContextSelector.ts b/prebuilt/react-native-sn-table/table/context/useContextSelector.ts new file mode 100644 index 00000000..06f8f74e --- /dev/null +++ b/prebuilt/react-native-sn-table/table/context/useContextSelector.ts @@ -0,0 +1,46 @@ +import { Context, useContext, useEffect, useMemo, useReducer, useRef } from 'react'; +import { getSelectorContext, SelectorContextType } from './createSelectorProvider'; + +type Selector = (state: TContext) => TSelected; + +export function useContextSelector(context: Context, selector: Selector): TSelected { + const accessorContext = getSelectorContext(context) as Context>; + const [accessor, listeners] = useContext(accessorContext); + const [, forceUpdate] = useReducer((dummy) => dummy + 1, 0); + + const latestSelector = useRef(selector); + const latestSelectedState = useRef(); + + const currentValue = accessor(); + + if (currentValue === undefined) { + throw new Error('You must call useContextSelector inside a valid context.'); + } + + const selectedState = useMemo(() => selector(currentValue), [currentValue, selector]); + + useEffect(() => { + latestSelector.current = selector; + latestSelectedState.current = selectedState; + }, [currentValue, selectedState, selector]); + + useEffect(() => { + const listener = (nextValue: T) => { + const newSelectedState = latestSelector.current && latestSelector.current(nextValue); + + if (newSelectedState !== latestSelectedState.current) { + forceUpdate(); + } + }; + + listeners.add(listener); + + return () => { + listeners.delete(listener); + }; + }, [listeners]); + + return selectedState; +} + +export default useContextSelector; diff --git a/prebuilt/react-native-sn-table/table/hooks/use-did-update-effect.js b/prebuilt/react-native-sn-table/table/hooks/use-did-update-effect.js new file mode 100644 index 00000000..91dc10ec --- /dev/null +++ b/prebuilt/react-native-sn-table/table/hooks/use-did-update-effect.js @@ -0,0 +1,13 @@ +import { useEffect, useRef } from 'react'; + +export default function useDidUpdateEffect(fn, inputs) { + const didMountRef = useRef(false); + + useEffect(() => { + if (didMountRef.current) { + fn(); + } else { + didMountRef.current = true; + } + }, inputs); +} diff --git a/prebuilt/react-native-sn-table/table/hooks/use-focus-listener.js b/prebuilt/react-native-sn-table/table/hooks/use-focus-listener.js new file mode 100644 index 00000000..13d61c84 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/hooks/use-focus-listener.js @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { handleFocusoutEvent } from '../utils/handle-accessibility'; + +const useFocusListener = (tableWrapperRef, shouldRefocus, keyboard) => { + useEffect(() => { + const memoedWrapper = tableWrapperRef.current; + if (!memoedWrapper) return () => {}; + + const focusOutCallback = (evt) => handleFocusoutEvent(evt, shouldRefocus, keyboard); + memoedWrapper.addEventListener('focusout', focusOutCallback); + + return () => { + memoedWrapper.removeEventListener('focusout', focusOutCallback); + }; + }, [tableWrapperRef, shouldRefocus, keyboard]); +}; + +export default useFocusListener; diff --git a/prebuilt/react-native-sn-table/table/hooks/use-scroll-listener.js b/prebuilt/react-native-sn-table/table/hooks/use-scroll-listener.js new file mode 100644 index 00000000..5b1ca03c --- /dev/null +++ b/prebuilt/react-native-sn-table/table/hooks/use-scroll-listener.js @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { handleHorizontalScroll } from '../utils/handle-scroll'; + +const useScrollListener = (tableContainerRef, direction) => { + useEffect(() => { + const memoedContainer = tableContainerRef.current; + if (!memoedContainer) return () => {}; + + const horizontalScrollCallback = (evt) => handleHorizontalScroll(evt, direction === 'rtl', memoedContainer); + memoedContainer.addEventListener('wheel', horizontalScrollCallback); + + return () => { + memoedContainer.removeEventListener('wheel', horizontalScrollCallback); + }; + }, [tableContainerRef, direction]); +}; + +export default useScrollListener; diff --git a/prebuilt/react-native-sn-table/table/mui-config.json b/prebuilt/react-native-sn-table/table/mui-config.json new file mode 100644 index 00000000..0a3df68e --- /dev/null +++ b/prebuilt/react-native-sn-table/table/mui-config.json @@ -0,0 +1,645 @@ +{ + "breakpoints": { + "keys": ["xs", "sm", "md", "lg", "xl"], + "values": { + "xs": 0, + "sm": 600, + "md": 960, + "lg": 1280, + "xl": 1920 + }, + "unit": "px" + }, + "direction": "ltr", + "components": { + "MuiIconButton": { + "styleOverrides": { + "root": { + "padding": 7, + "borderRadius": 2, + "border": "1px solid transparent", + "&.sprout-focus-visible": { + "borderColor": "#177FE6", + "boxShadow": "0 0 0 1px #177FE6" + }, + "&&&:hover": { + "backgroundColor": "transparent" + } + } + } + }, + "MuiButtonBase": { + "defaultProps": { + "disableRipple": true, + "disableTouchRipple": true, + "focusVisibleClassName": "sprout-focus-visible" + } + }, + "MuiFormLabel": { + "styleOverrides": { + "root": { + "color": "#404040", + "fontSize": 12, + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 600, + "fontStretch": "normal", + "fontStyle": "normal", + "letterSpacing": "normal", + "lineHeight": 1, + "&.Mui-focused": { + "color": "#404040" + }, + "&.Mui-error": { + "color": "rgba(0, 0, 0, 0.55)" + }, + "&[optional=true]": { + "&:after": { + "color": "rgba(0, 0, 0, 0.55)", + "content": "'(optional)'", + "fontWeight": 400, + "marginLeft": 4 + } + } + }, + "asterisk": { + "visibility": "collapse", + "display": "inline-flex", + "&::after": { + "visibility": "visible", + "color": "rgba(0, 0, 0, 0.55)", + "content": "'(required)'", + "fontWeight": 400, + "marginLeft": 4 + } + } + } + }, + "MuiOutlinedInput": { + "defaultProps": { + "notched": false + }, + "styleOverrides": { + "root": { + "fontSize": 14, + "lineHeight": 16, + "backgroundColor": "#FFFFFF", + "borderRadius": 3, + "&:not(.Mui-focused):not(.Mui-error):not(.Mui-disabled):hover .MuiOutlinedInput-notchedOutline": { + "border": "solid 1px rgba(0, 0, 0, 0.05)", + "borderColor": "rgba(0, 0, 0, 0.3)" + }, + "&.Mui-focused:not(.Mui-error) .MuiOutlinedInput-notchedOutline": { + "border": "solid 1px #177FE6", + "boxShadow": "0px 0px 0px 1px #177FE6, inset 0 2px 0 0 rgba(0, 0, 0, 0.05)" + }, + "&.Mui-focused.Mui-error .MuiOutlinedInput-notchedOutline": { + "border": "solid 1px #DC423F", + "boxShadow": "0px 0px 0px 1px #DC423F, inset 0 2px 0 0 rgba(0, 0, 0, 0.05)" + }, + "&.Mui-disabled": { + "backgroundColor": "transparent" + }, + "&.Mui-disabled .MuiOutlinedInput-notchedOutline": { + "border": "solid 1px rgba(0, 0, 0, 0.1)", + "borderColor": "rgba(0, 0, 0, 0.1)", + "backgroundColor": "rgba(0, 0, 0, 0.05)" + } + }, + "adornedStart": { + "paddingLeft": 0 + }, + "adornedEnd": { + "paddingRight": 0 + }, + "input": { + "color": "#404040", + "padding": "8px 12px", + "&.Mui-disabled": { + "opacity": 0.45 + } + }, + "notchedOutline": { + "borderColor": "rgba(0, 0, 0, 0.15)", + "borderRadius": 3, + "boxShadow": "inset 0 2px 0 0 rgba(0, 0, 0, 0.05)" + }, + "multiline": { + "padding": "0" + } + } + }, + "MuiInput": { + "styleOverrides": { + "root": { + "&.Mui-focused": { + "borderColor": "#177FE6", + "boxShadow": "0 0 0 2px #177FE6" + } + } + } + }, + "MuiInputBase": { + "styleOverrides": { + "input": { + "fontSize": 14, + "padding": "8px 12px", + "lineHeight": "16px", + "&.MuiInputBase-inputAdornedStart": { + "paddingLeft": 0 + }, + "&.MuiInputBase-inputAdornedEnd": { + "paddingRight": 0 + } + } + } + }, + "MuiInputLabel": { + "defaultProps": { + "shrink": true + }, + "styleOverrides": { + "outlined": { + "fontSize": 12, + "fontWeight": 600, + "color": "#404040", + "&.MuiInputLabel-shrink": { + "transform": "translate(0px, -16px)", + "fontWeight": 600 + } + } + } + }, + "MuiTab": { + "styleOverrides": { + "root": { + "fontSize": 14, + "fontWeight": 600, + "textTransform": "none", + "minWidth": "auto", + "boxSizing": "border-box", + "paddingBottom": 12, + "&:hover": { + "borderBottom": "2px solid #CCCCCC", + "paddingBottom": 10 + }, + "&.sprout-focus-visible": { + "boxShadow": "inset 0 0 0 2px #177FE6", + "backgroundColor": "rgba(0, 0, 0, 0.03)", + "borderRadius": 3 + }, + "&.Mui-disabled": { + "opacity": 0.3 + } + }, + "textColorPrimary": { + "color": "#404040", + "&.Mui-selected": { + "color": "#404040" + } + }, + "textColorInherit": { + "opacity": "unset" + }, + "labelIcon": { + "minWidth": 40, + "minHeight": 48 + } + } + }, + "MuiTableCell": { + "styleOverrides": { + "root": { + "padding": "0px 8px 0px 16px", + "borderBottom": "1px solid #D9D9D9", + "borderRight": "1px solid #D9D9D9", + "height": 39 + }, + "head": { + "height": 41, + "fontWeight": 600, + "backgroundColor": "#FAFAFA" + }, + "paddingCheckbox": { + "width": "40px", + "padding": "0px" + }, + "paddingNone": { + "padding": 0 + }, + "sizeSmall": { + "padding": "0px 8px", + "height": "29px", + "&.MuiTableCell-head": { + "height": "31px" + }, + "&.MuiTableCell-paddingCheckbox": { + "width": 40, + "padding": 0 + } + }, + "stickyHeader": { + "backgroundColor": "#FAFAFA", + "borderBottom": "1px solid #D9D9D9" + } + } + }, + "MuiTableContainer": { + "styleOverrides": { + "root": { + "borderBottom": "1px solid #D9D9D9", + "borderTop": "1px solid #D9D9D9" + } + } + }, + "MuiTableHead": { + "styleOverrides": { + "root": { + "backgroundColor": "#FAFAFA" + } + } + }, + "MuiTableRow": { + "styleOverrides": { + "root": { + "&.Mui-selected": { + "backgroundColor": "rgba(0, 0, 0, 0.05)", + "&:hover": { + "backgroundColor": "rgba(0, 0, 0, 0.03)" + } + }, + "&&:hover": { + "backgroundColor": "rgba(0, 0, 0, 0.03)" + } + } + } + }, + "MuiTableSortLabel": { + "defaultProps": {}, + "styleOverrides": { + "root": { + "display": "flex", + "justifyContent": "space-between", + "&:hover": { + "& icon": { + "opacity": 0.5 + } + }, + "&.Mui-active": { + "&& icon": { + "opacity": 1 + } + } + }, + "icon": { + "opacity": 0 + }, + "iconDirectionDesc": { + "transform": "rotate(0deg)" + }, + "iconDirectionAsc": { + "transform": "rotate(180deg)" + } + } + }, + "MuiTable": { + "styleOverrides": { + "root": { + "borderLeft": "1px solid #D9D9D9" + } + } + }, + "MuiToolbar": { + "defaultProps": { + "disableGutters": true + }, + "styleOverrides": { + "root": { + "display": "flex", + "flex": 1, + "padding": "0 16px" + }, + "dense": { + "minHeight": "46px", + "padding": "0 8px" + } + } + }, + "MuiNativeSelect": { + "styleOverrides": { + "select": { + "height": 32, + "zIndex": 1, + "borderRadius": 3, + "& em": { + "color": "rgba(0, 0, 0, 0.55)", + "WebkitFontSmoothing": "antialiased", + "&::after": { + "content": "''" + } + }, + "& ~i": { + "position": "absolute", + "right": "12px", + "padding": "6px" + }, + "&[aria-expanded=true]": { + "backgroundColor": "rgba(0, 0, 0, 0.05)" + }, + "& ~fieldset": { + "boxShadow": "none !important", + "border": "none !important", + "backgroundColor": "unset !important" + }, + "&:hover": { + "backgroundColor": "rgba(0, 0, 0, 0.03)" + }, + "&:active": { + "backgroundColor": "rgba(0, 0, 0, 0.1)" + }, + "&:focus": { + "backgroundColor": "rgba(0, 0, 0, 0.03)", + "borderRadius": "3px !important", + "border": "solid 1px #177FE6", + "boxShadow": "inset 0px 0px 0px 1px #177FE6" + }, + "&$disabled": { + "opacity": "unset", + "color": "rgba(0, 0, 0, 0.3)", + "&:hover": { + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgba(0, 0, 0, 0.15)" + }, + "& ~i": { + "color": "rgba(0, 0, 0, 0.3)" + } + } + }, + "selectMenu": { + "height": 14 + }, + "outlined": { + "border": "solid 1px rgba(0, 0, 0, 0.15)" + } + } + }, + "MuiFormControl": { + "styleOverrides": { + "root": { + "flexDirection": "row", + "alignItems": "center", + "paddingLeft": 8, + "paddingRight": 18 + } + } + } + }, + "palette": { + "mode": "light", + "primary": { + "light": "#0AAF54", + "main": "#009845", + "dark": "#0D6932", + "contrastText": "#FFFFFF" + }, + "secondary": { + "light": "#469DCD", + "main": "#3F8AB3", + "dark": "#2F607B", + "contrastText": "#FFFFFF" + }, + "error": { + "light": "#F05551", + "main": "#DC423F", + "dark": "#97322F", + "contrastText": "#FFFFFF" + }, + "warning": { + "light": "#FFC629", + "main": "#EF960F", + "dark": "#A4681C", + "contrastText": "#FFFFFF" + }, + "success": { + "light": "#0AAF54", + "main": "#009845", + "dark": "#0D6932", + "contrastText": "#FFFFFF" + }, + "info": { + "light": "#469DCD", + "main": "#3F8AB3", + "dark": "#2F607B", + "contrastText": "#FFFFFF" + }, + "text": { + "primary": "#404040", + "secondary": "rgba(0, 0, 0, 0.55)", + "disabled": "rgba(0, 0, 0, 0.3)" + }, + "action": { + "hover": "rgba(0, 0, 0, 0.03)", + "hoverOpacity": 0.05, + "active": "rgba(0, 0, 0, 0.54)", + "selected": "rgba(0, 0, 0, 0.08)", + "selectedOpacity": 0.08, + "disabled": "rgba(0, 0, 0, 0.26)", + "disabledBackground": "rgba(0, 0, 0, 0.12)", + "disabledOpacity": 0.38, + "focus": "rgba(0, 0, 0, 0.12)", + "focusOpacity": 0.12, + "activatedOpacity": 0.12 + }, + "amethyst": { + "main": "#655dc6", + "light": "#8D8BCE", + "dark": "#413885", + "contrastText": "#FFFFFF" + }, + "background": { + "paper": "#FFFFFF", + "default": "#F2F2F2" + }, + "divider": "rgba(0, 0, 0, 0.15)", + "common": { + "black": "#000", + "white": "#fff" + }, + "grey": { + "50": "#fafafa", + "100": "#f5f5f5", + "200": "#eeeeee", + "300": "#e0e0e0", + "400": "#bdbdbd", + "500": "#9e9e9e", + "600": "#757575", + "700": "#616161", + "800": "#424242", + "900": "#212121", + "A100": "#f5f5f5", + "A200": "#eeeeee", + "A400": "#bdbdbd", + "A700": "#616161" + }, + "contrastThreshold": 3, + "tonalOffset": 0.2 + }, + "shape": { + "borderRadius": 3 + }, + "shadows": [ + "none", + "0px 1px 2px 0px rgba(0,0,0,0.15)", + "0px 1px 2px 0px rgba(0,0,0,0.15)", + "0px 1px 2px 0px rgba(0,0,0,0.15)", + "0px 1px 2px 0px rgba(0,0,0,0.15)", + "0px 1px 2px 0px rgba(0,0,0,0.15)", + "0px 1px 2px 0px rgba(0,0,0,0.15)", + "0px 2px 4px 0px rgba(0,0,0,0.15)", + "0px 2px 4px 0px rgba(0,0,0,0.15)", + "0px 2px 4px 0px rgba(0,0,0,0.15)", + "0px 2px 4px 0px rgba(0,0,0,0.15)", + "0px 2px 4px 0px rgba(0,0,0,0.15)", + "0px 2px 4px 0px rgba(0,0,0,0.15)", + "0px 4px 10px 0px rgba(0,0,0,0.15)", + "0px 4px 10px 0px rgba(0,0,0,0.15)", + "0px 4px 10px 0px rgba(0,0,0,0.15)", + "0px 4px 10px 0px rgba(0,0,0,0.15)", + "0px 4px 10px 0px rgba(0,0,0,0.15)", + "0px 4px 10px 0px rgba(0,0,0,0.15)", + "0px 6px 20px 0px rgba(0,0,0,0.15)", + "0px 6px 20px 0px rgba(0,0,0,0.15)", + "0px 6px 20px 0px rgba(0,0,0,0.15)", + "0px 6px 20px 0px rgba(0,0,0,0.15)", + "0px 6px 20px 0px rgba(0,0,0,0.15)", + "0px 6px 20px 0px rgba(0,0,0,0.15)" + ], + "transitions": { + "easing": { + "easeInOut": "cubic-bezier(0.4, 0, 0.2, 1)", + "easeOut": "cubic-bezier(0.0, 0, 0.2, 1)", + "easeIn": "cubic-bezier(0.4, 0, 1, 1)", + "sharp": "cubic-bezier(0.4, 0, 0.6, 1)" + }, + "duration": { + "standard": 300, + "short": 250, + "enteringScreen": 225, + "shorter": 200, + "leavingScreen": 195, + "shortest": 150, + "complex": 375 + } + }, + "typography": { + "fontSize": 14, + "fontWeightLight": 300, + "fontWeightRegular": 400, + "fontWeightMedium": 600, + "fontWeightBold": 700, + "htmlFontSize": 14, + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "button": { + "fontWeight": 600, + "fontSize": 14, + "textTransform": "none", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "lineHeight": 1.75 + }, + "body1": { + "fontSize": 16, + "lineHeight": "24px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 400 + }, + "body2": { + "fontSize": 14, + "lineHeight": "20px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 400 + }, + "h1": { + "fontSize": 32, + "lineHeight": "32px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 300 + }, + "h2": { + "fontSize": 28, + "lineHeight": "32px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 300 + }, + "h3": { + "fontSize": 24, + "lineHeight": "24px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 400 + }, + "h4": { + "fontSize": 20, + "lineHeight": "24px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 400 + }, + "h5": { + "fontSize": 16, + "fontWeight": 600, + "lineHeight": "16px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif" + }, + "h6": { + "fontSize": 14, + "fontWeight": 600, + "lineHeight": "16px", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif" + }, + "caption": { + "color": "rgba(0, 0, 0, 0.55)", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 400, + "fontSize": "0.8571428571428571rem", + "lineHeight": 1.66 + }, + "subtitle1": { + "fontSize": 14, + "color": "rgba(0, 0, 0, 0.55)", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 400, + "lineHeight": 1.75 + }, + "subtitle2": { + "fontSize": 12, + "color": "rgba(0, 0, 0, 0.55)", + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 600, + "lineHeight": 1.57 + }, + "overline": { + "fontFamily": "'Source Sans Pro', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontWeight": 400, + "fontSize": "0.8571428571428571rem", + "lineHeight": 2.66, + "textTransform": "uppercase" + } + }, + "zIndex": { + "modal": 1200, + "snackbar": 1200, + "drawer": 1200, + "appBar": 1200, + "mobileStepper": 1200, + "tooltip": 1200, + "speedDial": 1050 + }, + "mixins": { + "toolbar": { + "minHeight": 56, + "@media (min-width:0px) and (orientation: landscape)": { + "minHeight": 48 + }, + "@media (min-width:600px)": { + "minHeight": 64 + } + } + } +} diff --git a/prebuilt/react-native-sn-table/table/mui-setup.js b/prebuilt/react-native-sn-table/table/mui-setup.js new file mode 100644 index 00000000..09d4273c --- /dev/null +++ b/prebuilt/react-native-sn-table/table/mui-setup.js @@ -0,0 +1,44 @@ +import { createTheme } from '@mui/material/styles'; +import * as muiConfig from './mui-config.json'; + +export default function muiSetup(direction) { + // Currently importing a reduced copy of sprout, should be replaced with the open-source version of sprout ASAP + if (muiConfig.components) { + muiConfig.components.MuiIconButton.styleOverrides.root.padding = '0px 7px'; + muiConfig.components.MuiTable.styleOverrides.root = {}; + muiConfig.components.MuiTableSortLabel.styleOverrides.root.color = 'inherit'; + muiConfig.components.MuiTableSortLabel.styleOverrides.root['&.Mui-active'].color = 'inherit'; + muiConfig.components.MuiTableSortLabel.styleOverrides.root['&:hover'].color = 'inherit'; + muiConfig.components.MuiTableRow.styleOverrides.root['&&:hover'].backgroundColor = 'rgba(0, 0, 0, 0)'; + muiConfig.components.MuiTableCell.styleOverrides.root.padding = '7px 14px'; + muiConfig.components.MuiTableCell.styleOverrides.root.height = 'auto'; + muiConfig.components.MuiTableCell.styleOverrides.root.lineHeight = '130%'; + muiConfig.components.MuiTableCell.styleOverrides.head.height = 'auto'; + muiConfig.components.MuiTableCell.styleOverrides.head.lineHeight = '150%'; + muiConfig.components.MuiTableCell.styleOverrides.root['&:focus'] = { + boxShadow: '0 0 0 2px #3f8ab3 inset', + outline: 'none', + }; + muiConfig.components.MuiTableContainer.styleOverrides.root = {}; + muiConfig.components.MuiFormLabel.styleOverrides.root['&.Mui-focused'] = { color: 'inherit' }; + muiConfig.components.MuiInputBase.styleOverrides.root = { + backgroundColor: 'inherit', + }; + muiConfig.components.MuiInputBase.styleOverrides.input.padding = '0px 12px'; + muiConfig.components.MuiInputBase.styleOverrides.input.border = '1px solid transparent'; + muiConfig.components.MuiOutlinedInput.styleOverrides.input.padding = '0px 12px'; + muiConfig.components.MuiNativeSelect.styleOverrides.outlined.border = '1px solid transparent'; + muiConfig.components.MuiInputLabel.styleOverrides.outlined = { + fontSize: 14, + width: 'fit-content', + position: 'relative', + padding: 8, + transform: 'none', + fontWeight: 400, + }; + muiConfig.components.MuiFormControl.styleOverrides.root.paddingLeft = 28; + muiConfig.components.MuiFormControl.styleOverrides.root.paddingRight = 14; + } + + return createTheme({ ...muiConfig, direction }); +} diff --git a/prebuilt/react-native-sn-table/table/native/ColumnWidthStorage.js b/prebuilt/react-native-sn-table/table/native/ColumnWidthStorage.js new file mode 100644 index 00000000..c60d1ffa --- /dev/null +++ b/prebuilt/react-native-sn-table/table/native/ColumnWidthStorage.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line import/prefer-default-export +export const getKey = (app, layout) => { + return `${app.id}.${layout?.qInfo?.qId}.v2`; +}; diff --git a/prebuilt/react-native-sn-table/table/native/DataCachesStream.js b/prebuilt/react-native-sn-table/table/native/DataCachesStream.js new file mode 100644 index 00000000..1c6a3c3e --- /dev/null +++ b/prebuilt/react-native-sn-table/table/native/DataCachesStream.js @@ -0,0 +1,25 @@ +export default class DataCacheStream { + constructor(manageData, qaeProps) { + this.manageData = manageData; + this.data = undefined; + this.slice = []; + this.dataPointer = { top: 0, bottom: 0, rows: [], size: { qcy: 0 } }; + this.qaeProps = qaeProps; + } + + async invalidate(model, layout, pageInfo) { + this.model = model; + this.layout = layout; + this.pageInfo = pageInfo; + this.data = await this.manageData(model, layout, pageInfo, null, this.qaeProps); + this.data.reset = true; + return this.data; + } + + async next() { + this.pageInfo = { ...this.pageInfo, page: this.pageInfo.page + 1 }; + this.data = await this.manageData(this.model, this.layout, this.pageInfo, null, this.qaeProps); + this.data.reset = false; + return this.data; + } +} diff --git a/prebuilt/react-native-sn-table/table/native/Props.js b/prebuilt/react-native-sn-table/table/native/Props.js new file mode 100644 index 00000000..33167725 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/native/Props.js @@ -0,0 +1,13 @@ +const RowProps = Object.freeze({ + rowHeight: 40, + headerHeight: 40, + borderRadius: 4, + headerBackgroundColor: '#F0F0F0', + borderBackgroundColor: '#DFDFDF', + borderSelectedColor: 'black', + selectedBackground: '#009845', + headerTextColor: 'black', +}); + +// eslint-disable-next-line import/prefer-default-export +export { RowProps }; diff --git a/prebuilt/react-native-sn-table/table/native/SelectionCaches.js b/prebuilt/react-native-sn-table/table/native/SelectionCaches.js new file mode 100644 index 00000000..ffd63b3c --- /dev/null +++ b/prebuilt/react-native-sn-table/table/native/SelectionCaches.js @@ -0,0 +1,45 @@ +class SelectionCaches { + constructor(selectionsAPI) { + this.selectionsAPI = selectionsAPI; + } + + async toggleSelected(value) { + try { + if (value.length > 0) { + if (!this.selectionsAPI.isModal()) { + await this.selectionsAPI.begin(['/qHyperCubeDef']); + } + const elms = new Set(); + const rows = new Set(); + value.forEach((e) => { + const split = e.split('/'); + split.shift(); + rows.add(parseInt(split[2], 10)); + elms.add(parseInt(split[1], 10)); + }); + + const params = ['/qHyperCubeDef', Array.from(rows), Array.from(elms)]; + const s = { + method: 'selectHyperCubeCells', + params, + }; + + this.selectionsAPI.select(s); + } else if (this.selectionsAPI.isModal()) { + await this.selectionsAPI.cancel(); + } + } catch (error) { + console.log('Error toggling selections', error); + } + } + + async confirm() { + try { + this.selectionsAPI.confirm(); + } catch (error) { + console.log(error); + } + } +} + +export default SelectionCaches; diff --git a/prebuilt/react-native-sn-table/table/native/Table.jsx b/prebuilt/react-native-sn-table/table/native/Table.jsx new file mode 100644 index 00000000..a6c9c264 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/native/Table.jsx @@ -0,0 +1,215 @@ +/* eslint-disable no-restricted-syntax */ +import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; +import { View, StyleSheet, useWindowDimensions } from 'react-native'; +import PropTypes from 'prop-types'; +import { SimpleGrid } from '@qlik/react-native-simple-grid'; +import { RowProps } from './Props'; +import SelectionCaches from './SelectionCaches'; +import DataCacheStream from './DataCachesStream'; +import { getKey } from './ColumnWidthStorage'; +import { getBodyCellStyle, getHeaderStyle } from '../utils/styling-utils'; + +const styles = StyleSheet.create({ + body: { + flex: 1, + overflow: 'hidden', + }, + table: { + flex: 1, + overflow: 'hidden', + }, +}); + +function transformRepresentation(element, theme) { + try { + const cloned = JSON.parse(JSON.stringify(element)); + if (cloned.representation?.miniChart?.colors && theme) { + const { colors } = cloned.representation.miniChart; + for (const [key, value] of Object.entries(colors)) { + if ((colors[key]?.color !== 'none' && value.index > 0) || !colors[key].color) { + colors[key].index -= 1; + colors[key].color = theme.getColorPickerColor(colors[key]); + } + } + cloned.representation.miniChart.colors = colors; + } + return cloned?.representation; + } catch (error) { + console.log(error); + } + return undefined; +} + +const Table = ({ + layout, + model, + manageData, + selectionsAPI, + changeSortOrder, + app, + rect, + theme, + translator, + qaeProps, + themeData +}) => { + const selectionsCaches = useRef(new SelectionCaches(selectionsAPI)); + const [tableData, setTableData] = useState(undefined); + const [clearSelections, setClearSelections] = useState('no'); + const dataStreamCaches = useRef(new DataCacheStream(manageData, qaeProps)); + const tableTheme = useRef({ ...RowProps }); + const dims = useWindowDimensions(); + + const name = useMemo(() => { + return getKey(app, layout); + }, [app, layout]); + + const translations = useMemo( + () => ({ + menu: { + copy: translator?.translate?.('Copy') || 'Copy', + expand: translator?.translate?.('ExpandRow') || 'Expand Row', + share: translator?.translate?.('Share') || 'Share', + }, + misc: { + of: translator?.translate?.('Of') || 'of', + }, + headerValues: [translator?.translate?.('ColumnValue'), translator?.translate?.('RowValue')], + }), + [translator] + ); + + const contentStyle = useMemo(() => { + const cellStyle = getBodyCellStyle(layout, theme); + const headerStyle = getHeaderStyle(layout, theme); + cellStyle.fontSize = parseInt(cellStyle.fontSize, 10); + headerStyle.fontSize = parseInt(headerStyle.fontSize, 10); + return { cellStyle, headerStyle }; + }, [layout, theme]); + + useEffect(() => { + const handleData = async () => { + try { + const data = await dataStreamCaches.current.invalidate(model, layout, { + page: 0, + rowsPerPage: 100, + rowsPerPageOptions: [], + }); + + const { qHyperCube } = layout; + const activeSortHeader = qHyperCube?.qEffectiveInterColumnSortOrder[0] || 0; + data.columns = data.columns.map((e) => ({ + ...e, + active: e.dataColIdx === activeSortHeader, + representation: transformRepresentation(e, theme), + })); + + setTableData(data); + } catch (error) { + console.log('error', error); + } + }; + handleData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [app, layout, model]); + + const onHeaderPressed = useCallback( + async (event) => { + try { + const column = JSON.parse(event.nativeEvent.column); + if (column) { + changeSortOrder(layout, column, true); + } + } catch (error) { + console.log('error with sorting', error); + } + }, + [changeSortOrder, layout] + ); + + useEffect(() => { + const canceledSub = selectionsAPI.on('canceled', () => { + setClearSelections('yes'); + }); + const clearedSub = selectionsAPI.on('cleared', () => { + setClearSelections('yes'); + }); + const confirmedSub = selectionsAPI.on('confirmed', () => { + setClearSelections('yes'); + }); + + return () => { + canceledSub?.remove(); + clearedSub?.remove(); + confirmedSub?.remove(); + }; + }, [selectionsAPI]); + + useEffect(() => { + if (selectionsAPI) { + selectionsAPI.cancel(); + setClearSelections('yes'); + } + }, [dims, selectionsAPI]); + + const onEndReached = useCallback(async () => { + const data = await dataStreamCaches.current.next(); + if (data) { + const { qHyperCube } = layout; + const activeSortHeader = qHyperCube?.qEffectiveInterColumnSortOrder[0] || 0; + data.columns = data.columns.map((e) => ({ + ...e, + active: e.dataColIdx === activeSortHeader, + representation: transformRepresentation(e, theme), + })); + setTableData(data); + } + }, [layout, theme]); + + const onSelectionsChanged = useCallback((event) => { + selectionsCaches.current.toggleSelected(event.nativeEvent.selections); + setClearSelections('no'); + }, []); + + const onConfirmSelections = useCallback(() => { + setClearSelections('yes'); + selectionsCaches.current.confirm(); + }, []); + + return tableData ? ( + + + + ) : null; +}; + +Table.propTypes = { + layout: PropTypes.object.isRequired, + model: PropTypes.object.isRequired, + app: PropTypes.object.isRequired, + manageData: PropTypes.func.isRequired, + selectionsAPI: PropTypes.object.isRequired, + changeSortOrder: PropTypes.func.isRequired, + rect: PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, + translator: PropTypes.object.isRequired, + qaeProps: PropTypes.object.isRequired, +}; + +export default Table; diff --git a/prebuilt/react-native-sn-table/table/styles.js b/prebuilt/react-native-sn-table/table/styles.js new file mode 100644 index 00000000..9b3b8aa2 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/styles.js @@ -0,0 +1,159 @@ +import { + styled, + Paper, + TableContainer, + TableRow, + TableSortLabel, + TableBody, + Select, + IconButton, + TableCell, +} from '@mui/material'; + +// ---------- AnnounceWrapper ---------- + +export const TableAnnouncer = styled('div')({ + clip: 'rect(0 0 0 0)', + clipPath: 'inset(50%)', + height: '1px', + overflow: 'hidden', + position: 'absolute', + whiteSpace: 'nowrap', + width: '1px', +}); + +// ---------- FooterWrapper ---------- + +export const StyledFooterWrapper = styled(Paper, { + shouldForwardProp: (prop) => prop !== 'tableTheme', +})(({ tableTheme, theme }) => ({ + height: 48, + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + paddingRight: theme.spacing(1), + boxShadow: 'none', + borderStyle: 'solid', + borderWidth: '0px 0px 1px 0px', + borderRadius: 0, + borderColor: tableTheme.pagination.borderColor, + color: tableTheme.pagination.color, + backgroundColor: tableTheme.backgroundColor, +})); + +// ---------- PaginationContent ---------- + +export const StyledSelect = styled(Select, { + shouldForwardProp: (prop) => prop !== 'paginationTheme', +})(({ paginationTheme }) => ({ + backgroundColor: 'inherit', + '& .MuiNativeSelect-icon': { color: paginationTheme.iconColor }, +})); + +export const StyledIconButton = styled(IconButton, { + shouldForwardProp: (prop) => prop !== 'disabledCondition' && prop !== 'paginationTheme', +})(({ disabledCondition, paginationTheme }) => ({ + color: disabledCondition ? paginationTheme.disabledIconColor : paginationTheme.iconColor, + cursor: disabledCondition ? 'default' : 'pointer', + height: '32px', +})); + +// ---------- TableBodyWrapper ---------- + +export const StyledTableBody = styled(TableBody, { + shouldForwardProp: (prop) => prop !== 'paginationNeeded' && prop !== 'bodyCellStyle', +})(({ paginationNeeded, bodyCellStyle }) => ({ + 'tr :last-child': { + borderRight: paginationNeeded && 0, + }, + 'tr :first-child': { + borderLeft: !paginationNeeded && '1px solid rgb(217, 217, 217)', + }, + '& td, th': { + fontSize: bodyCellStyle.fontSize, + padding: bodyCellStyle.padding, + }, +})); + +export const StyledBodyRow = styled(TableRow, { + shouldForwardProp: (prop) => prop !== 'bodyCellStyle', +})(({ hover, bodyCellStyle }) => + hover + ? { + '&&:hover': { + '& td:not(.selected), th:not(.selected)': { + backgroundColor: bodyCellStyle.hoverBackgroundColor, + color: bodyCellStyle.hoverFontColor, + }, + }, + } + : {} +); + +// ---------- TableHeadWrapper ---------- + +export const StyledHeadRow = styled(TableRow, { + shouldForwardProp: (prop) => prop !== 'paginationNeeded', +})(({ paginationNeeded }) => ({ + '& :last-child': { + borderRight: paginationNeeded && 0, + }, + 'th:first-of-type': { + borderLeft: !paginationNeeded && '1px solid rgb(217, 217, 217)', + }, +})); + +export const StyledSortLabel = styled(TableSortLabel, { + shouldForwardProp: (prop) => prop !== 'headerStyle', +})(({ headerStyle }) => ({ + '&.Mui-active .MuiTableSortLabel-icon': { + color: headerStyle.sortLabelColor, + }, +})); + +export const VisuallyHidden = styled('span')({ + border: 0, + clip: 'rect(0 0 0 0)', + height: 1, + margin: -1, + overflow: 'hidden', + padding: 0, + position: 'absolute', + top: 20, + width: 1, +}); + +// ---------- TableTotals ---------- + +export const StyledTotalsCell = styled(TableCell, { + shouldForwardProp: (prop) => prop !== 'isTop' && prop !== 'headRowHeight' && prop !== 'totalsStyle', +})(({ totalsStyle, isTop, headRowHeight }) => ({ + ...totalsStyle, + fontWeight: 'bold', + position: 'sticky', + borderWidth: isTop ? '0px 1px 2px 0px' : '2px 1px 1px 0px', + top: isTop && headRowHeight, + bottom: !isTop && 0, + marginTop: 0, +})); + +// ---------- TableWrapper ---------- + +export const StyledTableWrapper = styled(Paper, { + shouldForwardProp: (prop) => prop !== 'tableTheme' && prop !== 'paginationNeeded', +})(({ tableTheme, paginationNeeded }) => ({ + borderWidth: paginationNeeded ? '0px 1px 0px' : '0px', + borderStyle: 'solid', + borderColor: tableTheme.borderColor, + height: '100%', + backgroundColor: tableTheme.tableBackgroundColorFromTheme, + boxShadow: 'none', + borderRadius: 'unset', +})); + +export const StyledTableContainer = styled(TableContainer, { + shouldForwardProp: (prop) => prop !== 'fullHeight' && prop !== 'constraints', +})(({ fullHeight, constraints }) => ({ + height: fullHeight ? '100%' : 'calc(100% - 49px)', + overflow: constraints.active ? 'hidden' : 'auto', +})); diff --git a/prebuilt/react-native-sn-table/table/utils/__tests__/color-utils.spec.js b/prebuilt/react-native-sn-table/table/utils/__tests__/color-utils.spec.js new file mode 100644 index 00000000..96b6a432 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/__tests__/color-utils.spec.js @@ -0,0 +1,205 @@ +import { resolveToRGBAorRGB, isDarkColor, isTransparentColor, removeOpacity } from '../color-utils'; + +describe('color-utils', () => { + describe('resolveToRGBAorRGB', () => { + let color = '#fff'; + + it('should resolve a hex color to rgb', () => { + let result = resolveToRGBAorRGB(color); + expect(result).toBe('rgb(255,255,255)'); + + color = '#4f4f4f'; + + result = resolveToRGBAorRGB(color); + expect(result).toBe('rgb(79,79,79)'); + + color = '#3f4g5p'; + + result = resolveToRGBAorRGB(color); + expect(result).toBe('none'); + }); + it('should resolve a hex color term to rgba', () => { + color = '#00000000'; + + let result = resolveToRGBAorRGB(color); + expect(result).toBe('rgba(0,0,0,0)'); + + color = '#3240'; + + result = resolveToRGBAorRGB(color); + expect(result).toBe('rgba(51,34,68,0)'); + + color = '#11111100'; + + result = resolveToRGBAorRGB(color); + expect(result).toBe('rgba(17,17,17,0)'); + }); + it('should resolve a color term to rgb', () => { + color = 'red'; + + const result = resolveToRGBAorRGB(color); + expect(result).toBe('rgb(255,0,0)'); + }); + it('should return to rgba when color is the transparent term', () => { + color = 'transparent'; + + const result = resolveToRGBAorRGB(color); + expect(result).toBe('rgba(255,255,255,0)'); + }); + it('should return when nothing is provided', () => { + const result = resolveToRGBAorRGB(); + expect(result).toBe('none'); + }); + }); + + describe('isDarkColor', () => { + let color = 'black'; + + it('should be true when the the color term is a dark color', () => { + const result = isDarkColor(color); + expect(result).toBe(true); + }); + it('should be false when the color term is a light color', () => { + color = 'white'; + + const result = isDarkColor(color); + expect(result).toBe(false); + }); + it('should be false when the color is transparent', () => { + color = 'transparent'; + + const result = isDarkColor(color); + expect(result).toBe(false); + }); + it('should be false when the color is inherit', () => { + color = 'inherit'; + + const result = isDarkColor(color); + expect(result).toBe(false); + }); + it('should be false when the color is undefined', () => { + color = 'undefined'; + + const result = isDarkColor(color); + expect(result).toBe(false); + }); + it('should be false when the color is a light color in hex code', () => { + color = '#ffffff'; + + const result = isDarkColor(color); + expect(result).toBe(false); + }); + it('should be true when the color is a dark color in hex code', () => { + color = '#000000'; + + const result = isDarkColor(color); + expect(result).toBe(true); + }); + it('should be false when the color is a light color in rgb', () => { + color = 'rgba(255, 255, 255)'; + + const result = isDarkColor(color); + expect(result).toBe(false); + }); + + it('should be false when the color is a light color in rgba - case 2', () => { + color = 'rgba(0, 0, 0, 0.1)'; + + const result = isDarkColor(color); + expect(result).toBe(true); + }); + it('should be true when the color is a dark color in rgb', () => { + color = 'rgb(0, 0, 0)'; + + const result = isDarkColor(color); + expect(result).toBe(true); + }); + it('should be true when the color is a dark color in rgba', () => { + color = 'rgba(0, 0, 0, 0.9)'; + + const result = isDarkColor(color); + expect(result).toBe(true); + }); + it('should be true when the color is a transparent color in rgba', () => { + color = 'rgba(0, 0, 0, 0)'; + + const result = isDarkColor(color); + expect(result).toBe(true); + }); + }); + + describe('isTransparentColor', () => { + let color = 'black'; + + it('should be false when the the color is a color term', () => { + const result = isTransparentColor(color); + expect(result).toBe(false); + }); + it('should be false when the the color is opaque in rgba', () => { + color = 'rgba(0, 0, 0, 0.9)'; + + const result = isTransparentColor(color); + expect(result).toBe(false); + }); + it('should be false when the the color is opaque in rgb', () => { + color = 'rgb(0, 0, 0)'; + + const result = isTransparentColor(color); + expect(result).toBe(false); + }); + it('should be false when the the color is opaque in hex', () => { + color = '#000000'; + + const result = isTransparentColor(color); + expect(result).toBe(false); + }); + + it('should be false when nothing is provided', () => { + const result = isTransparentColor(); + expect(result).toBe(false); + }); + it('should be true when the color is transparent in rgba', () => { + color = 'rgba(0, 0, 0, 0)'; + + const result = isTransparentColor(color); + expect(result).toBe(true); + }); + it('should be true when the color is transparent in hex', () => { + color = '#00000000'; + + const result = isTransparentColor(color); + expect(result).toBe(true); + }); + it('should be true when the color is transparent', () => { + color = 'transparent'; + + const result = isTransparentColor(color); + expect(result).toBe(true); + }); + }); + + describe('removeOpacity', () => { + it('should remove the opacity from a color', () => { + const color = 'rgba(0, 0, 0, 0.9)'; + + const result = removeOpacity(color); + expect(result).toBe('rgb(0,0,0)'); + }); + it('should remove the opacity from a transparent color term', () => { + const color = 'transparent'; + + const result = removeOpacity(color); + expect(result).toBe('rgb(255,255,255)'); + }); + it('should remove the opacity from a transparent color in hex', () => { + const color = '#00000000'; + + const result = removeOpacity(color); + expect(result).toBe('rgb(0,0,0)'); + }); + it('should return undefined when nothing is provided', () => { + const result = removeOpacity(); + expect(result).toBe(undefined); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/utils/__tests__/get-cell-renderer.spec.ts b/prebuilt/react-native-sn-table/table/utils/__tests__/get-cell-renderer.spec.ts new file mode 100644 index 00000000..c77671c4 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/__tests__/get-cell-renderer.spec.ts @@ -0,0 +1,53 @@ +import getCellRenderer from '../get-cell-renderer'; +import * as withSelections from '../../components/withSelections'; +import * as withColumnStyling from '../../components/withColumnStyling'; +import * as withStyling from '../../components/withStyling'; + +describe('render', () => { + describe('getCellRenderer', () => { + let selectionsEnabled: boolean; + let hasColumnStyling: boolean; + + beforeEach(() => { + selectionsEnabled = false; + hasColumnStyling = false; + jest.spyOn(withSelections, 'default').mockImplementation(() => jest.fn()); + jest.spyOn(withColumnStyling, 'default').mockImplementation(() => jest.fn()); + jest.spyOn(withStyling, 'default').mockImplementation(() => jest.fn()); + }); + + afterEach(() => jest.clearAllMocks()); + + it('should call withStyling when selectionsEnabled and hasColumnStyling is false', () => { + getCellRenderer(hasColumnStyling, selectionsEnabled); + expect(withStyling.default).toHaveBeenCalledTimes(1); + expect(withSelections.default).not.toHaveBeenCalled(); + expect(withColumnStyling.default).not.toHaveBeenCalled(); + }); + it('should call withStyling and withSelections when selectionsEnabled is true and hasColumnStyling is false', () => { + selectionsEnabled = true; + + getCellRenderer(hasColumnStyling, selectionsEnabled); + expect(withStyling.default).toHaveBeenCalledTimes(1); + expect(withSelections.default).toHaveBeenCalledTimes(1); + expect(withColumnStyling.default).not.toHaveBeenCalled(); + }); + it('should call withStyling and withColumnStyling when selectionsEnabled is false and hasColumnStyling is true', () => { + hasColumnStyling = true; + + getCellRenderer(hasColumnStyling, selectionsEnabled); + expect(withStyling.default).toHaveBeenCalledTimes(1); + expect(withColumnStyling.default).toHaveBeenCalledTimes(1); + expect(withSelections.default).not.toHaveBeenCalled(); + }); + it('should call all with-functions when both selectionsEnabled and hasColumnStyling are true', () => { + selectionsEnabled = true; + hasColumnStyling = true; + + getCellRenderer(hasColumnStyling, selectionsEnabled); + expect(withStyling.default).toHaveBeenCalledTimes(1); + expect(withColumnStyling.default).toHaveBeenCalledTimes(1); + expect(withSelections.default).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/utils/__tests__/handle-accessiblity.spec.js b/prebuilt/react-native-sn-table/table/utils/__tests__/handle-accessiblity.spec.js new file mode 100644 index 00000000..af87368c --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/__tests__/handle-accessiblity.spec.js @@ -0,0 +1,392 @@ +import * as handleAccessibility from '../handle-accessibility'; + +describe('handle-accessibility', () => { + let cell; + let rootElement; + let focusedCellCoord; + let setFocusedCellCoord; + let keyboard; + + beforeEach(() => { + cell = { focus: jest.fn(), blur: jest.fn(), setAttribute: jest.fn() }; + rootElement = { + getElementsByClassName: () => [{ getElementsByClassName: () => [cell] }], + querySelector: () => cell, + }; + focusedCellCoord = [0, 0]; + setFocusedCellCoord = jest.fn(); + keyboard = { focus: jest.fn(), focusSelection: jest.fn(), enabled: true }; + }); + + afterEach(() => jest.clearAllMocks()); + + describe('updateFocus', () => { + let focusType; + + beforeEach(() => { + focusType = 'focus'; + }); + + it('should focus cell and call setAttribute when focusType is focus', () => { + handleAccessibility.updateFocus({ focusType, cell }); + expect(cell.focus).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '0'); + }); + + it('should blur cell and call setAttribute when focusType is blur', () => { + focusType = 'blur'; + + handleAccessibility.updateFocus({ focusType, cell }); + expect(cell.blur).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '-1'); + }); + + it('should call setAttribute when focusType is addTab', () => { + focusType = 'addTab'; + + handleAccessibility.updateFocus({ focusType, cell }); + expect(cell.focus).not.toHaveBeenCalled(); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '0'); + }); + + it('should call setAttribute when focusType is removeTab', () => { + focusType = 'removeTab'; + + handleAccessibility.updateFocus({ focusType, cell }); + expect(cell.blur).not.toHaveBeenCalled(); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '-1'); + }); + + it('should early return and not throw error when cell is undefined', () => { + cell = undefined; + + expect(() => handleAccessibility.updateFocus({ focusType, cell })).not.toThrow(); + }); + }); + + describe('findCellWithTabStop', () => { + const elementCreator = (type, tabIdx) => { + const targetElement = global.document.createElement(type); + targetElement.setAttribute('tabIndex', tabIdx); + return targetElement; + }; + + beforeEach(() => { + rootElement = { + querySelector: () => { + if ((cell.tagName === 'TD' || cell.tagName === 'TH') && cell.getAttribute('tabIndex') === '0') return cell; + return null; + }, + }; + }); + + it('should return active td element', () => { + cell = elementCreator('td', '0'); + + const cellElement = handleAccessibility.findCellWithTabStop(rootElement); + + expect(cellElement).not.toBeNull(); + expect(cellElement.tagName).toBe('TD'); + expect(cellElement.getAttribute('tabIndex')).toBe('0'); + }); + + it('should return active th element', () => { + cell = elementCreator('th', '0'); + const cellElement = handleAccessibility.findCellWithTabStop(rootElement); + + expect(cellElement).not.toBeNull(); + expect(cellElement.tagName).toBe('TH'); + expect(cellElement.getAttribute('tabIndex')).toBe('0'); + }); + + it('should return null', () => { + cell = elementCreator('div', '-1'); + + const cellElement = handleAccessibility.findCellWithTabStop(rootElement); + + expect(cellElement).toBeNull(); + }); + }); + + describe('handleClickToFocusBody', () => { + let totalsPosition; + + const cellData = { + rawRowIdx: 0, + rawColIdx: 0, + }; + + beforeEach(() => { + totalsPosition = 'noTotals'; + }); + + it('should indirectly call setFocusedCellCoord with adjusted index, and keyboard.focus', () => { + handleAccessibility.handleClickToFocusBody(cellData, rootElement, setFocusedCellCoord, keyboard, totalsPosition); + expect(cell.setAttribute).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '-1'); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledWith([1, 0]); + expect(keyboard.focus).toHaveBeenCalledTimes(1); + }); + + it('should indirectly call setFocusedCellCoord with adjusted index, but not keyboard.focus when keyboard.enabled is falsey', () => { + keyboard.enabled = false; + + handleAccessibility.handleClickToFocusBody(cellData, rootElement, setFocusedCellCoord, keyboard, totalsPosition); + expect(cell.setAttribute).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '-1'); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledWith([1, 0]); + expect(keyboard.focus).not.toHaveBeenCalled(); + }); + + it('should indirectly call setFocusedCellCoord with index adjusted for totals on top, and keyboard.focus', () => { + totalsPosition = 'top'; + + handleAccessibility.handleClickToFocusBody(cellData, rootElement, setFocusedCellCoord, keyboard, totalsPosition); + expect(cell.setAttribute).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '-1'); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledWith([2, 0]); + expect(keyboard.focus).toHaveBeenCalledTimes(1); + }); + }); + + describe('handleClickToFocusHead', () => { + const columnIndex = 2; + + it('should indirectly call updateFocus, setFocusedCellCoord and keyboard.focus', () => { + handleAccessibility.handleClickToFocusHead(columnIndex, rootElement, setFocusedCellCoord, keyboard); + expect(cell.setAttribute).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '-1'); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledWith([0, 2]); + expect(keyboard.focus).toHaveBeenCalledTimes(1); + }); + }); + + describe('handleMouseDownLabelToFocusHeadCell', () => { + const evt = { preventDefault: jest.fn() }; + const columnIndex = 0; + + it('should indirectly call updateFocus, setFocusedCellCoord and keyboard.focus', () => { + handleAccessibility.handleMouseDownLabelToFocusHeadCell(evt, rootElement, columnIndex); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(cell.focus).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledTimes(1); + expect(cell.setAttribute).toHaveBeenCalledWith('tabIndex', '0'); + }); + }); + + describe('handleResetFocus', () => { + let shouldRefocus; + let isSelectionMode; + let announce; + + const resetFocus = () => + handleAccessibility.handleResetFocus({ + focusedCellCoord, + rootElement, + shouldRefocus, + isSelectionMode, + setFocusedCellCoord, + keyboard, + announce, + }); + + beforeEach(() => { + focusedCellCoord = [2, 1]; + shouldRefocus = { current: false }; + isSelectionMode = false; + keyboard = { enabled: true, active: true }; + announce = jest.fn(); + }); + + it('should only remove tabIndex when keyboard.enabled is true and keyboard.active is false', () => { + keyboard = { enabled: true, active: false }; + + resetFocus(); + expect(cell.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledWith([0, 0]); + expect(cell.focus).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('should set tabindex on the first cell and not focus', () => { + resetFocus(); + expect(cell.setAttribute).toHaveBeenCalledTimes(2); + expect(setFocusedCellCoord).toHaveBeenCalledWith([0, 0]); + expect(cell.focus).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('should set tabindex on the first cell and focus when shouldRefocus is true', () => { + shouldRefocus.current = true; + + resetFocus(); + expect(cell.setAttribute).toHaveBeenCalledTimes(2); + expect(setFocusedCellCoord).toHaveBeenCalledWith([0, 0]); + expect(cell.focus).toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('should set tabindex on the second cell in currently focused column when isSelectionMode is true', () => { + isSelectionMode = true; + const row = { getElementsByClassName: () => [cell, cell] }; + rootElement = { + getElementsByClassName: () => [row, row], + querySelector: () => cell, + }; + + resetFocus(); + expect(cell.setAttribute).toHaveBeenCalledTimes(2); + expect(setFocusedCellCoord).toHaveBeenCalledWith([1, 1]); + expect(cell.focus).not.toHaveBeenCalled(); + }); + + it('should announce cell content and selection status for non selected first cell after focusing on it', () => { + cell = { ...cell, textContent: '#something' }; + const row = { getElementsByClassName: () => [cell, cell] }; + rootElement = { + getElementsByClassName: () => [row, row], + querySelector: () => cell, + }; + isSelectionMode = true; + + resetFocus(); + expect(announce).toHaveBeenCalledWith({ + keys: ['#something,', 'SNTable.SelectionLabel.NotSelectedValue'], + }); + }); + + it('should announce cell content and selection status for selected first cell after focusing on it', () => { + const tmpCell = global.document.createElement('td'); + tmpCell.classList.add('selected'); + + cell = { ...cell, classList: tmpCell.classList, textContent: '#something' }; + const row = { getElementsByClassName: () => [cell, cell] }; + rootElement = { + getElementsByClassName: () => [row, row], + querySelector: () => cell, + }; + isSelectionMode = true; + + resetFocus(); + expect(announce).toHaveBeenCalledWith({ + keys: ['#something,', 'SNTable.SelectionLabel.SelectedValue'], + }); + }); + }); + + describe('handleFocusoutEvent', () => { + let containsRelatedTarget; + let evt; + let shouldRefocus; + let announcement1; + let announcement2; + + beforeEach(() => { + containsRelatedTarget = false; + announcement1 = { innerHTML: 'firstAnnouncement' }; + announcement2 = { innerHTML: 'secondAnnouncement' }; + evt = { + currentTarget: { + contains: () => containsRelatedTarget, + querySelector: (identifier) => (identifier.slice(-1) === '1' ? announcement1 : announcement2), + }, + }; + shouldRefocus = { current: false }; + keyboard = { enabled: true, blur: jest.fn() }; + }); + + it('should call blur and remove announcements when currentTarget does not contain relatedTarget, shouldRefocus is false and keyboard.enabled is true', () => { + handleAccessibility.handleFocusoutEvent(evt, shouldRefocus, keyboard); + expect(keyboard.blur).toHaveBeenCalledWith(false); + expect(announcement1.innerHTML).toBe(''); + expect(announcement2.innerHTML).toBe(''); + }); + + it('should not call blur when currentTarget contains relatedTarget', () => { + containsRelatedTarget = true; + + handleAccessibility.handleFocusoutEvent(evt, shouldRefocus, keyboard); + expect(keyboard.blur).not.toHaveBeenCalled(); + }); + + it('should not call blur when shouldRefocus is true', () => { + shouldRefocus.current = true; + + handleAccessibility.handleFocusoutEvent(evt, shouldRefocus, keyboard); + expect(keyboard.blur).not.toHaveBeenCalled(); + }); + + it('should not call blur when keyboard.enabled is falsey', () => { + keyboard.enabled = false; + + handleAccessibility.handleFocusoutEvent(evt, shouldRefocus, keyboard); + expect(keyboard.blur).not.toHaveBeenCalled(); + }); + }); + + describe('focusSelectionToolbar', () => { + let element; + let parentElement; + let last; + + beforeEach(() => { + parentElement = { focus: jest.fn() }; + element = { + closest: () => ({ querySelector: () => ({ parentElement }) }), + }; + last = false; + }); + + it('should call parentElement.focus when clientConfirmButton exists', () => { + handleAccessibility.focusSelectionToolbar(element, keyboard, last); + expect(parentElement.focus).toHaveBeenCalledTimes(1); + expect(keyboard.focusSelection).not.toHaveBeenCalled(); + }); + + it("should call keyboard.focusSelection when clientConfirmButton doesn't exist", () => { + parentElement = null; + handleAccessibility.focusSelectionToolbar(element, keyboard, last); + expect(keyboard.focusSelection).toHaveBeenCalledWith(false); + }); + }); + + describe('announceSelectionState', () => { + let isSelected; + let announce; + let nextCell; + let isSelectionMode; + + beforeEach(() => { + isSelected = false; + announce = jest.fn(); + nextCell = { + classList: { + contains: () => isSelected, + }, + }; + isSelectionMode = false; + }); + + it('should do nothing when not in selection mode', () => { + handleAccessibility.announceSelectionState(announce, nextCell, isSelectionMode); + expect(announce).not.toHaveBeenCalled(); + }); + + it('should call announce with SelectedValue key when in selection mode and value is selected', () => { + isSelectionMode = true; + isSelected = true; + handleAccessibility.announceSelectionState(announce, nextCell, isSelectionMode); + expect(announce).toHaveBeenCalledWith({ keys: ['SNTable.SelectionLabel.SelectedValue'] }); + }); + + it('should Call announce with NotSelectedValue key when in selection mode and value is not selected', () => { + isSelectionMode = true; + handleAccessibility.announceSelectionState(announce, nextCell, isSelectionMode); + expect(announce).toHaveBeenCalledWith({ keys: ['SNTable.SelectionLabel.NotSelectedValue'] }); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/utils/__tests__/handle-key-press.spec.js b/prebuilt/react-native-sn-table/table/utils/__tests__/handle-key-press.spec.js new file mode 100644 index 00000000..f922c7b4 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/__tests__/handle-key-press.spec.js @@ -0,0 +1,792 @@ +import { + handleTableWrapperKeyDown, + arrowKeysNavigation, + headHandleKeyPress, + bodyHandleKeyPress, + bodyHandleKeyUp, + handleLastTab, + totalHandleKeyPress, +} from '../handle-key-press'; + +import * as handleAccessibility from '../handle-accessibility'; + +describe('handle-key-press', () => { + describe('handleTableWrapperKeyDown', () => { + let evt = {}; + let totalRowCount; + let page; + let rowsPerPage; + let handleChangePage; + let setShouldRefocus; + let keyboard; + let isSelectionMode; + + beforeEach(() => { + evt = { + shiftKey: true, + ctrlKey: true, + metaKey: true, + key: 'ArrowRight', + stopPropagation: () => {}, + preventDefault: () => {}, + }; + handleChangePage = jest.fn(); + setShouldRefocus = jest.fn(); + }); + + it('when shift key is not pressed, handleChangePage should not run', () => { + evt.shiftKey = false; + handleTableWrapperKeyDown({ evt, totalRowCount, page, rowsPerPage, handleChangePage, setShouldRefocus }); + expect(handleChangePage).not.toHaveBeenCalled(); + expect(setShouldRefocus).not.toHaveBeenCalled(); + }); + + it('when ctrl key or meta key is not pressed, handleChangePage should not run', () => { + evt.ctrlKey = false; + evt.metaKey = false; + handleTableWrapperKeyDown({ evt, totalRowCount, page, rowsPerPage, handleChangePage, setShouldRefocus }); + expect(handleChangePage).not.toHaveBeenCalled(); + expect(setShouldRefocus).not.toHaveBeenCalled(); + }); + + it('when press arrow right key on the first page which contains all rows, handleChangePage should not run', () => { + page = 0; + totalRowCount = 40; + rowsPerPage = 40; + handleTableWrapperKeyDown({ evt, totalRowCount, page, rowsPerPage, handleChangePage, setShouldRefocus }); + expect(handleChangePage).not.toHaveBeenCalled(); + expect(setShouldRefocus).not.toHaveBeenCalled(); + }); + + it('when press arrow left key on the first page, handleChangePage should not run', () => { + evt.key = 'ArrowLeft'; + page = 0; + totalRowCount = 40; + rowsPerPage = 10; + handleTableWrapperKeyDown({ evt, totalRowCount, page, rowsPerPage, handleChangePage, setShouldRefocus }); + expect(handleChangePage).not.toHaveBeenCalled(); + expect(setShouldRefocus).not.toHaveBeenCalled(); + }); + + it('when press arrow right key on the page whose next page contains rows, should change page', () => { + totalRowCount = 40; + page = 0; + rowsPerPage = 10; + handleTableWrapperKeyDown({ evt, totalRowCount, page, rowsPerPage, handleChangePage, setShouldRefocus }); + expect(handleChangePage).toHaveBeenCalledTimes(1); + expect(setShouldRefocus).toHaveBeenCalledTimes(1); + }); + + it('when press arrow left key not on the first page, should change page', () => { + evt.key = 'ArrowLeft'; + totalRowCount = 40; + page = 1; + rowsPerPage = 40; + handleTableWrapperKeyDown({ evt, totalRowCount, page, rowsPerPage, handleChangePage, setShouldRefocus }); + expect(handleChangePage).toHaveBeenCalledTimes(1); + expect(setShouldRefocus).toHaveBeenCalledTimes(1); + }); + + it('when press escape is pressed and keyboard.enabled is true, should call keyboard.blur', () => { + evt = { + key: 'Escape', + stopPropagation: jest.fn(), + preventDefault: jest.fn(), + }; + keyboard = { enabled: true, blur: jest.fn() }; + handleTableWrapperKeyDown({ + evt, + totalRowCount, + page, + rowsPerPage, + handleChangePage, + setShouldRefocus, + keyboard, + }); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(keyboard.blur).toHaveBeenCalledWith(true); + }); + + it('should ignore keyboard.blur while you are focusing on the pagination and pressing Esc key', () => { + evt = { + key: 'Escape', + stopPropagation: jest.fn(), + preventDefault: jest.fn(), + }; + keyboard = { enabled: true, blur: jest.fn() }; + isSelectionMode = true; + handleTableWrapperKeyDown({ + evt, + totalRowCount, + page, + rowsPerPage, + handleChangePage, + setShouldRefocus, + keyboard, + isSelectionMode, + }); + expect(evt.preventDefault).not.toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).not.toHaveBeenCalledTimes(1); + expect(keyboard.blur).not.toHaveBeenCalledTimes(1); + }); + }); + + describe('arrowKeysNavigation', () => { + let evt; + const rowAndColumnCount = {}; + let rowIndex; + let colIndex; + + beforeEach(() => { + evt = {}; + rowAndColumnCount.rowCount = 1; + rowAndColumnCount.columnCount = 1; + rowIndex = 0; + colIndex = 0; + }); + + it('should stay the current cell when move down', () => { + evt.key = 'ArrowDown'; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(0); + expect(nextCol).toBe(0); + }); + + it('should stay the current cell when move up', () => { + evt.key = 'ArrowUp'; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(0); + expect(nextCol).toBe(0); + }); + + it('should go to one row down cell', () => { + evt.key = 'ArrowDown'; + rowAndColumnCount.rowCount = 2; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(1); + expect(nextCol).toBe(0); + }); + + it('should go to one row up cell', () => { + evt.key = 'ArrowUp'; + rowAndColumnCount.rowCount = 2; + rowIndex = 1; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(0); + expect(nextCol).toBe(0); + }); + + it('should go to one column left cell', () => { + evt.key = 'ArrowLeft'; + rowAndColumnCount.columnCount = 2; + colIndex = 1; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(0); + expect(nextCol).toBe(0); + }); + + it('should go to one column right cell', () => { + evt.key = 'ArrowRight'; + rowAndColumnCount.columnCount = 2; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(0); + expect(nextCol).toBe(1); + }); + + it('should stay the current cell when other keys are pressed', () => { + evt.key = 'Control'; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(0); + expect(nextCol).toBe(0); + }); + + it('should move to the next row when you reach to the end of the current row', () => { + evt.key = 'ArrowRight'; + rowAndColumnCount.rowCount = 3; + rowAndColumnCount.columnCount = 3; + rowIndex = 1; + colIndex = 3; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(2); + expect(nextCol).toBe(0); + }); + + it('should move to the prev row when we reach to the beginning of the current row', () => { + evt.key = 'ArrowLeft'; + rowAndColumnCount.rowCount = 3; + rowAndColumnCount.columnCount = 3; + rowIndex = 2; + colIndex = 0; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(1); + expect(nextCol).toBe(2); + }); + + it('should stay at the first row and first col of table when we reached to the beginning of the table', () => { + evt.key = 'ArrowLeft'; + rowAndColumnCount.rowCount = 2; + rowAndColumnCount.columnCount = 2; + rowIndex = 0; + colIndex = 0; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(0); + expect(nextCol).toBe(0); + }); + + it('should stay at the end row and end col of table when you reached to the end of the table', () => { + evt.key = 'ArrowRight'; + rowAndColumnCount.rowCount = 2; + rowAndColumnCount.columnCount = 2; + rowIndex = 1; + colIndex = 1; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex]); + expect(nextRow).toBe(1); + expect(nextCol).toBe(1); + }); + + it('should stay at the current cell when topAllowed cell is 1 and trying to move up from rowIdx 1', () => { + evt.key = 'ArrowUp'; + const topAllowedRow = 1; + rowAndColumnCount.rowCount = 3; + rowIndex = 1; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex], topAllowedRow); + expect(nextRow).toBe(1); + expect(nextCol).toBe(0); + }); + + it('should stay at the current cell when trying to move left and topAllowedRow is > 0 (i.e in selection mode', () => { + evt.key = 'ArrowLeft'; + const topAllowedRow = 1; + rowAndColumnCount.rowCount = 3; + rowAndColumnCount.colCount = 3; + rowIndex = 1; + colIndex = 1; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex], topAllowedRow); + expect(nextRow).toBe(1); + expect(nextCol).toBe(1); + }); + + it('should stay at the current cell when trying to move right and topAllowedRow is > 0 (i.e in selection mode', () => { + evt.key = 'ArrowRight'; + const topAllowedRow = 1; + rowAndColumnCount.rowCount = 3; + rowAndColumnCount.colCount = 3; + rowIndex = 1; + colIndex = 1; + const [nextRow, nextCol] = arrowKeysNavigation(evt, rowAndColumnCount, [rowIndex, colIndex], topAllowedRow); + expect(nextRow).toBe(1); + expect(nextCol).toBe(1); + }); + }); + + describe('headHandleKeyPress', () => { + let rowIndex; + let colIndex; + let column; + let evt = {}; + let rootElement = {}; + let changeSortOrder; + let layout; + let isSortingEnabled; + let setFocusedCellCoord; + + const callHeadHandleKeyPress = () => + headHandleKeyPress({ + evt, + rootElement, + cellCoord: [rowIndex, colIndex], + column, + changeSortOrder, + layout, + isSortingEnabled, + setFocusedCellCoord, + }); + + beforeEach(() => { + rowIndex = 0; + colIndex = 0; + column = {}; + evt = { + key: 'ArrowDown', + stopPropagation: jest.fn(), + preventDefault: jest.fn(), + target: { + blur: jest.fn(), + setAttribute: jest.fn(), + }, + }; + rootElement = { + getElementsByClassName: () => [{ getElementsByClassName: () => [{ focus: () => {}, setAttribute: () => {} }] }], + }; + changeSortOrder = jest.fn(); + isSortingEnabled = true; + setFocusedCellCoord = jest.fn(); + }); + + it('when press arrow down key on head cell, should prevent default behavior, remove current focus and set focus and attribute to the next cell', () => { + callHeadHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.target.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + }); + + it('when press space bar key, should update the sorting', () => { + evt.key = ' '; + callHeadHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(changeSortOrder).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + + it('when press space bar key and sorting is not enabled, should not update the sorting', () => { + evt.key = ' '; + isSortingEnabled = false; + callHeadHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(changeSortOrder).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + + it('when press enter key, should update the sorting', () => { + evt.key = 'Enter'; + callHeadHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(changeSortOrder).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + + it('when press enter key and sorting is not enabled, should not update the sorting', () => { + evt.key = 'Enter'; + isSortingEnabled = false; + callHeadHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(changeSortOrder).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + + it('when press ArrowRight and shift and ctrl key, should not update the sorting', () => { + evt.key = 'ArrowRight'; + evt.shiftKey = true; + evt.ctrlKey = true; + callHeadHandleKeyPress(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(changeSortOrder).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + }); + + describe('totalHandleKeyPress', () => { + let evt = {}; + let rootElement = {}; + let setFocusedCellCoord; + let cellCoord; + + beforeEach(() => { + evt = { + key: 'ArrowDown', + stopPropagation: jest.fn(), + preventDefault: jest.fn(), + target: { + blur: jest.fn(), + setAttribute: jest.fn(), + }, + }; + cellCoord = [1, 1]; + rootElement = { + getElementsByClassName: () => [{ getElementsByClassName: () => [{ focus: () => {}, setAttribute: () => {} }] }], + }; + setFocusedCellCoord = jest.fn(); + }); + + it('should move the focus from the current cell to the next when arrow key down is pressed on a total cell', () => { + totalHandleKeyPress(evt, rootElement, cellCoord, setFocusedCellCoord); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.target.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + }); + + it('should not move the focus to the next cell when press ArrowRight and shift and ctrl key', () => { + evt.key = 'ArrowRight'; + evt.shiftKey = true; + evt.ctrlKey = true; + totalHandleKeyPress(evt, rootElement, cellCoord, setFocusedCellCoord); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + + it('should take the default case when the pressed key is not an arrow key', () => { + evt.key = 'Enter'; + totalHandleKeyPress(evt, rootElement, cellCoord, setFocusedCellCoord); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + }); + + describe('bodyHandleKeyPress', () => { + let rowIndex; + let colIndex; + let evt = {}; + let rootElement = {}; + let selectionsAPI; + let cell = []; + let selectionDispatch; + let isSelectionsEnabled; + let setFocusedCellCoord; + let isModal; + let keyboard; + let announce; + let paginationNeeded; + + const runBodyHandleKeyPress = () => + bodyHandleKeyPress({ + evt, + rootElement, + cellCoord: [rowIndex, colIndex], + selectionsAPI, + cell, + selectionDispatch, + isSelectionsEnabled, + setFocusedCellCoord, + announce, + keyboard, + paginationNeeded, + }); + + beforeEach(() => { + rowIndex = 0; + colIndex = 0; + isModal = false; + evt = { + key: 'ArrowDown', + stopPropagation: jest.fn(), + preventDefault: jest.fn(), + target: { + blur: jest.fn(), + setAttribute: jest.fn(), + }, + }; + rootElement = { + getElementsByClassName: () => [{ getElementsByClassName: () => [{ focus: () => {}, setAttribute: () => {} }] }], + }; + selectionsAPI = { + confirm: jest.fn(), + cancel: jest.fn(), + isModal: () => isModal, + }; + cell = { qElemNumber: 1, colIdx: 1, rowIdx: 1, isSelectable: true }; + keyboard = { enabled: true }; + selectionDispatch = jest.fn(); + isSelectionsEnabled = true; + setFocusedCellCoord = jest.fn(); + announce = jest.fn(); + paginationNeeded = true; + jest.spyOn(handleAccessibility, 'focusSelectionToolbar').mockImplementation(() => jest.fn()); + jest.spyOn(handleAccessibility, 'announceSelectionState').mockImplementation(() => jest.fn()); + }); + + afterEach(() => jest.clearAllMocks()); + + it('when press arrow down key on body cell, should prevent default behavior, remove current focus and set focus and attribute to the next cell', () => { + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.target.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(handleAccessibility.announceSelectionState).toHaveBeenCalledTimes(1); + }); + + it('when press shift + arrow down key on body cell, should prevent default behavior, remove current focus and set focus and attribute to the next cell, and select values for dimension', () => { + cell.nextQElemNumber = 1; + evt.shiftKey = true; + + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.target.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(selectionDispatch).toHaveBeenCalledTimes(1); + expect(handleAccessibility.announceSelectionState).not.toHaveBeenCalled(); + }); + + it('when press shift + arrow down key on the last row cell, should prevent default behavior, remove current focus and set focus and attribute to the next cell, but not select values for dimension', () => { + cell.nextQElemNumber = undefined; + evt.shiftKey = true; + + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.target.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(selectionDispatch).not.toHaveBeenCalled(); + expect(handleAccessibility.announceSelectionState).toHaveBeenCalledTimes(1); + }); + + it('when press shift + arrow up key on body cell, should prevent default behavior, remove current focus and set focus and attribute to the next cell, and select values for dimension', () => { + cell.prevQElemNumber = 1; + evt.shiftKey = true; + evt.key = 'ArrowUp'; + + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.target.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(selectionDispatch).toHaveBeenCalledTimes(1); + expect(handleAccessibility.announceSelectionState).not.toHaveBeenCalled(); + }); + + it('when press shift + arrow up key on the second row cell, should prevent default behavior, remove current focus and set focus and attribute to the next cell, but not select values for dimension', () => { + cell.prevQElemNumber = undefined; + evt.shiftKey = true; + evt.key = 'ArrowUp'; + + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.target.setAttribute).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).toHaveBeenCalledTimes(1); + expect(selectionDispatch).not.toHaveBeenCalled(); + expect(handleAccessibility.announceSelectionState).toHaveBeenCalledTimes(1); + }); + + it('when press space bar key and dimension, should select value for dimension', () => { + evt.key = ' '; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(selectionDispatch).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + + it('when press space bar key on a cell that is not selectable, should not select value', () => { + evt.key = ' '; + cell = { + isSelectable: false, + }; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(selectionDispatch).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when press space bar key and selections are not enabled, should not select value', () => { + evt.key = ' '; + isSelectionsEnabled = false; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(selectionDispatch).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when press enter key, should confirms selections', () => { + evt.key = 'Enter'; + isModal = true; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(selectionsAPI.confirm).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + expect(announce).toHaveBeenCalledWith({ keys: ['SNTable.SelectionLabel.SelectionsConfirmed'] }); + }); + + it('when press enter key and not in selections mode, should not confirms selections', () => { + evt.key = 'Enter'; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(selectionsAPI.confirm).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when press esc key and in selections mode, should cancel selection', () => { + evt.key = 'Escape'; + isModal = true; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(selectionsAPI.cancel).toHaveBeenCalledTimes(1); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + expect(announce).toHaveBeenCalledWith({ keys: ['SNTable.SelectionLabel.ExitedSelectionMode'] }); + }); + + it('when press esc key not in selection mode, should not cancel selection', () => { + evt.key = 'Escape'; + runBodyHandleKeyPress(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(selectionsAPI.cancel).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when press ArrowRight and shift and ctrl key, should not update the sorting', () => { + evt.key = 'ArrowRight'; + evt.shiftKey = true; + evt.ctrlKey = true; + runBodyHandleKeyPress(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(selectionsAPI.cancel).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when shift + tab is pressed and in selections mode, should prevent default and call focusSelectionToolbar', () => { + evt.key = 'Tab'; + evt.shiftKey = true; + isModal = true; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(handleAccessibility.focusSelectionToolbar).toHaveBeenCalledTimes(1); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when only tab is pressed should not prevent default nor call focusSelectionToolbar', () => { + evt.key = 'Tab'; + isModal = true; + runBodyHandleKeyPress(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when tab is pressed and paginatioNeeded is false, should prevent default and call focusSelectionToolbar', () => { + evt.key = 'Tab'; + isModal = true; + paginationNeeded = false; + runBodyHandleKeyPress(); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(handleAccessibility.focusSelectionToolbar).toHaveBeenCalledTimes(1); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when shift + tab is pressed but not in selection mode, should not prevent default nor call focusSelectionToolbar', () => { + evt.key = 'Tab'; + evt.shiftKey = true; + runBodyHandleKeyPress(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + }); + + it('when shift + tab is pressed but keyboard.enabled is false, should not prevent default nor call focusSelectionToolbar', () => { + evt.key = 'Tab'; + evt.shiftKey = true; + keyboard.enabled = false; + runBodyHandleKeyPress(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + expect(announce).not.toHaveBeenCalled(); + }); + + it('when other keys are pressed, should not do anything', () => { + evt.key = 'Control'; + runBodyHandleKeyPress(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(evt.target.blur).not.toHaveBeenCalled(); + expect(evt.target.setAttribute).not.toHaveBeenCalled(); + expect(selectionsAPI.cancel).not.toHaveBeenCalled(); + expect(setFocusedCellCoord).not.toHaveBeenCalled(); + }); + }); + + describe('bodyHandleKeyUp', () => { + let evt = {}; + let selectionDispatch; + + beforeEach(() => { + evt = { + key: 'Shift', + }; + selectionDispatch = jest.fn(); + }); + + it('when the shift key is pressed, should run selectionDispatch', () => { + bodyHandleKeyUp(evt, selectionDispatch); + + expect(selectionDispatch).toHaveBeenCalledTimes(1); + }); + + it('when other keys are pressed, should not do anything', () => { + evt.key = 'Control'; + + bodyHandleKeyUp(evt, selectionDispatch); + + expect(selectionDispatch).not.toHaveBeenCalled(); + }); + }); + + describe('handleLastTab', () => { + let evt; + let isSelectionMode; + + beforeEach(() => { + evt = { + key: 'Tab', + shiftKey: false, + target: {}, + stopPropagation: jest.fn(), + preventDefault: jest.fn(), + }; + isSelectionMode = true; + jest.spyOn(handleAccessibility, 'focusSelectionToolbar').mockImplementation(() => jest.fn()); + }); + + afterEach(() => jest.clearAllMocks()); + + it('should call focusSelectionToolbar when isSelectionMode is true and tab is pressed', () => { + handleLastTab(evt, isSelectionMode); + + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + expect(handleAccessibility.focusSelectionToolbar).toHaveBeenCalledTimes(1); + }); + + it('should not call focusSelectionToolbar when isSelectionMode is false', () => { + isSelectionMode = false; + handleLastTab(evt, isSelectionMode); + + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + }); + + it('should not call focusSelectionToolbar when key is not tab', () => { + evt.key = 'someKey'; + handleLastTab(evt, isSelectionMode); + + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + }); + + it('should not call focusSelectionToolbar when shift+tab is pressed', () => { + evt.shiftKey = true; + handleLastTab(evt, isSelectionMode); + + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + expect(handleAccessibility.focusSelectionToolbar).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/utils/__tests__/handle-scroll.spec.js b/prebuilt/react-native-sn-table/table/utils/__tests__/handle-scroll.spec.js new file mode 100644 index 00000000..3ad4fc87 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/__tests__/handle-scroll.spec.js @@ -0,0 +1,170 @@ +import { handleHorizontalScroll, handleNavigateTop } from '../handle-scroll'; + +describe('handle-scroll', () => { + describe('handleHorizontalScroll', () => { + let evt; + let memoedContainer; + let isRTL; + + beforeEach(() => { + evt = { + stopPropagation: jest.fn(), + preventDefault: jest.fn(), + deltaX: -1, + }; + memoedContainer = { + scrollWidth: 200, + offsetWidth: 100, + scrollLeft: 0, + }; + isRTL = true; + }); + + it('should run preventDefault when the scrollbar is at its leftmost place and is scrolled left in LTR direction', () => { + isRTL = false; + handleHorizontalScroll(evt, isRTL, memoedContainer); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('should run preventDefault when the scrollbar is at its rightmost place and is scrolled right in LTR direction', () => { + evt = { + ...evt, + deltaX: 1, + }; + memoedContainer = { + ...memoedContainer, + scrollLeft: 100.5, + }; + isRTL = false; + handleHorizontalScroll(evt, isRTL, memoedContainer); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('should not run preventDefault when the scrollbar is not at its leftmost place or the rightmost place in LTR direction', () => { + isRTL = false; + memoedContainer = { + ...memoedContainer, + scrollLeft: 50, + }; + handleHorizontalScroll(evt, isRTL, memoedContainer); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.preventDefault).not.toHaveBeenCalled(); + }); + + it('should early return when the it does not scroll horizontally', () => { + isRTL = false; + evt = { + ...evt, + deltaX: 0, + }; + const result = handleHorizontalScroll(evt, isRTL, memoedContainer); + + expect(result).toBe(undefined); + expect(evt.stopPropagation).not.toHaveBeenCalled(); + expect(evt.preventDefault).not.toHaveBeenCalled(); + }); + + it('should run preventDefault when the scrollbar is at its leftmost place and is scrolled left in RTL direction', () => { + evt = { + ...evt, + deltaX: -1, + }; + memoedContainer = { + ...memoedContainer, + scrollLeft: -100, + }; + handleHorizontalScroll(evt, isRTL, memoedContainer); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('should run preventDefault when the scrollbar is at its rightmost place and and is scrolled right in RTL direction', () => { + evt = { + ...evt, + deltaX: 1, + }; + memoedContainer = { + ...memoedContainer, + scrollLeft: 1, + }; + handleHorizontalScroll(evt, isRTL, memoedContainer); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.preventDefault).toHaveBeenCalledTimes(1); + }); + + it('should not run preventDefault when the scrollbar is not at its leftmost place or the rightmost place in RTL direction', () => { + evt = { + ...evt, + deltaX: -10, + }; + memoedContainer = { + ...memoedContainer, + scrollLeft: -50, + }; + handleHorizontalScroll(evt, isRTL, memoedContainer); + expect(evt.stopPropagation).toHaveBeenCalledTimes(1); + expect(evt.preventDefault).not.toHaveBeenCalled(); + }); + }); + + describe('handleNavigateTop', () => { + let rowHeight; + let scrollTo; + let tableContainerRef; + let focusedCellCoord; + let rootElement; + + beforeEach(() => { + rowHeight = 100; + scrollTo = jest.fn(); + tableContainerRef = { current: { scrollTo } }; + focusedCellCoord = [0, 0]; + rootElement = {}; + }); + + it('should not do anything when ref is not setup yet', () => { + tableContainerRef.current = {}; + + handleNavigateTop({ tableContainerRef, focusedCellCoord, rootElement }); + expect(scrollTo).not.toHaveBeenCalled(); + }); + + it('should the scrollbar is at its top when you reach the top two rows', () => { + focusedCellCoord = [1, 0]; + + handleNavigateTop({ tableContainerRef, focusedCellCoord, rootElement }); + expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' }); + }); + + it('should scroll upwards automatically if it detects the cursor gets behind ', () => { + const SCROLL_TOP_IDX = 7; + focusedCellCoord = [8, 0]; + tableContainerRef = { current: { scrollTo, scrollTop: SCROLL_TOP_IDX * rowHeight } }; + rootElement = { + getElementsByClassName: (query) => { + if (query === 'sn-table-head-cell') { + return [{ offsetHeight: 128 }]; + } + + return Array.from(Array(10).keys()).map((idx) => { + const rowCell = { + offsetHeight: rowHeight, + offsetTop: idx * rowHeight, + }; + + return { getElementsByClassName: () => [rowCell] }; + }); + }, + }; + // targetOffsetTop = tableContainer.current.scrollTop - cell.offsetHeight - tableHead.offsetHeight; + // 700 - 100 - 128 = 472 => so our scrollTo function migth be called with 600 + const targetOffsetTop = 472; + + handleNavigateTop({ tableContainerRef, focusedCellCoord, rootElement }); + expect(scrollTo).toHaveBeenCalledTimes(1); + expect(scrollTo).toHaveBeenCalledWith({ top: targetOffsetTop, behavior: 'smooth' }); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/utils/__tests__/selections-util.spec.ts b/prebuilt/react-native-sn-table/table/utils/__tests__/selections-util.spec.ts new file mode 100644 index 00000000..a603b053 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/__tests__/selections-util.spec.ts @@ -0,0 +1,414 @@ +import { stardust } from '@nebula.js/stardust'; +import { + addSelectionListeners, + reducer, + handleAnnounceSelectionStatus, + getSelectedRows, + getCellSelectionState, + SelectionStates, + SelectionActions, + SelectMultiValuesAction, + SelectAction, + ResetAction, + ClearAction, +} from '../selections-utils'; +import { TableCell, SelectionState, ExtendedSelectionAPI, AnnounceFn, ContextValue } from '../../../types'; + +describe('selections-utils', () => { + describe('addSelectionListeners', () => { + const listenerNames = ['deactivated', 'canceled', 'confirmed', 'cleared']; + let api: ExtendedSelectionAPI; + let selectionDispatch: jest.Mock; + let setShouldRefocus: jest.Mock; + let keyboard: stardust.Keyboard; + let containsActiveElement: boolean; + let tableWrapperRef: React.MutableRefObject; + + beforeEach(() => { + selectionDispatch = jest.fn(); + setShouldRefocus = jest.fn(); + api = { + on: jest.fn(), + removeListener: jest.fn(), + } as unknown as ExtendedSelectionAPI; + keyboard = { enabled: true, active: false, blur: jest.fn() }; + containsActiveElement = true; + tableWrapperRef = { + current: { + contains: () => containsActiveElement, + }, + } as unknown as React.MutableRefObject; + }); + + afterEach(() => jest.clearAllMocks()); + + it('should call api.on and api removeListener for all listeners', () => { + addSelectionListeners({ api, selectionDispatch, setShouldRefocus, keyboard, tableWrapperRef })?.(); + + listenerNames.forEach((name, index) => { + expect(api.on).toHaveBeenNthCalledWith(index + 1, name, expect.anything()); + }); + }); + it('should call api.on with the same callback for all listener names, that calls selectionDispatch', () => { + const callbacks: Array<() => void> = []; + api = { + on: (_: string, cb: () => void) => { + callbacks.push(cb); + }, + removeListener: () => null, + } as unknown as ExtendedSelectionAPI; + + addSelectionListeners({ api, selectionDispatch, setShouldRefocus, keyboard, tableWrapperRef }); + callbacks.forEach((cb) => { + cb(); + expect(selectionDispatch).toHaveBeenCalledWith({ type: SelectionActions.RESET }); + }); + // only for confirm events + expect(setShouldRefocus).toHaveBeenCalledTimes(1); + expect(keyboard.blur).not.toHaveBeenCalled(); + }); + it('should call keyboard blur when confirmed callback is called, keyboard.enabled is true and tableWrapperRef does not contain activeElement', () => { + containsActiveElement = false; + let confirmCallback: () => void; + api = { + on: (name: string, cb: () => void) => { + if (name === 'confirmed') confirmCallback = cb; + }, + removeListener: () => null, + } as unknown as ExtendedSelectionAPI; + + addSelectionListeners({ api, selectionDispatch, setShouldRefocus, keyboard, tableWrapperRef }); + // @ts-ignore ts does not understand that this has been assigned in the api.on() call + confirmCallback(); + expect(setShouldRefocus).not.toHaveBeenCalled(); + expect(keyboard.blur).toHaveBeenCalledTimes(1); + }); + + it('should not call keyboard blur when confirmed callback is called, keyboard.enabled is undefined and tableWrapperRef does not contain activeElement', () => { + containsActiveElement = false; + keyboard.enabled = false; + let confirmCallback; + api = { + on: (name: string, cb: () => void) => { + name === 'confirmed' && (confirmCallback = cb); + }, + removeListener: () => null, + } as unknown as ExtendedSelectionAPI; + + addSelectionListeners({ api, selectionDispatch, setShouldRefocus, keyboard, tableWrapperRef }); + // @ts-ignore ts does not understand that this has been assigned in the api.on() call + confirmCallback(); + expect(setShouldRefocus).not.toHaveBeenCalled(); + expect(keyboard.blur).not.toHaveBeenCalled(); + }); + }); + + describe('reducer', () => { + let state: SelectionState; + let cell: TableCell; + + beforeEach(() => { + state = { + rows: { 1: 1 }, + colIdx: 1, + api: { + isModal: () => false, + begin: jest.fn(), + select: jest.fn(), + cancel: jest.fn(), + } as unknown as ExtendedSelectionAPI, + isSelectMultiValues: false, + }; + cell = { qElemNumber: 1, colIdx: 1, rowIdx: 1 } as TableCell; + }); + + afterEach(() => jest.clearAllMocks()); + + describe('select', () => { + let action: SelectAction; + let announce: jest.Mock; + + beforeEach(() => { + announce = jest.fn(); + action = { + type: SelectionActions.SELECT, + payload: { + evt: { + shiftKey: false, + } as React.KeyboardEvent, + announce, + cell, + }, + }; + }); + + it('should call begin, select and announce when type is select and no previous selections', () => { + state.rows = {}; + state.colIdx = -1; + const params = ['/qHyperCubeDef', [cell.rowIdx], [cell.colIdx]]; + + const newState = reducer(state, action); + expect(newState).toEqual({ ...state, rows: { [cell.qElemNumber]: cell.rowIdx }, colIdx: cell.colIdx }); + expect(state.api.begin).toHaveBeenCalledTimes(1); + expect(state.api.select).toHaveBeenCalledWith({ method: 'selectHyperCubeCells', params }); + expect(state.api.cancel).not.toHaveBeenCalled(); + expect(action.payload.announce).toHaveBeenCalledTimes(1); + }); + + it('should call begin and announce but not select when type is select and isSelectMultiValues is true', () => { + state.rows = {}; + state.colIdx = -1; + action.payload = { + evt: { + shiftKey: true, + key: 'DownArrow', + } as React.KeyboardEvent, + announce, + cell, + }; + + const newState = reducer(state, action); + expect(newState).toEqual({ + ...state, + rows: { [cell.qElemNumber]: cell.rowIdx, [cell.prevQElemNumber]: cell.rowIdx - 1 }, + colIdx: cell.colIdx, + isSelectMultiValues: true, + }); + expect(state.api.begin).toHaveBeenCalledTimes(1); + expect(state.api.select).not.toHaveBeenCalled(); + expect(state.api.cancel).not.toHaveBeenCalled(); + expect(action.payload.announce).toHaveBeenCalledTimes(1); + }); + + it('should not call begin but call cancel and announce when same qElemNumber (resulting in empty selectedCells)', () => { + const newState = reducer(state, action); + expect(newState).toEqual({ ...state, rows: {}, colIdx: -1 }); + expect(state.api.begin).not.toHaveBeenCalled(); + expect(state.api.select).not.toHaveBeenCalled(); + expect(state.api.cancel).toHaveBeenCalledWith(); + expect(action.payload.announce).toHaveBeenCalledTimes(1); + }); + + it('should return early when excluded columns', () => { + cell.colIdx = 2; + + const newState = reducer(state, action); + expect(newState).toBe(state); + + expect(state.api.begin).not.toHaveBeenCalled(); + expect(state.api.cancel).not.toHaveBeenCalled(); + expect(state.api.select).not.toHaveBeenCalled(); + expect(action.payload.announce).not.toHaveBeenCalled(); + }); + }); + + describe('other', () => { + it('should call select when type is selectMultiValues, isSelectMultiValues is true and return isSelectMultiValues to be false', () => { + const action = { type: SelectionActions.SELECT_MULTI_VALUES } as SelectMultiValuesAction; + state.isSelectMultiValues = true; + const params = ['/qHyperCubeDef', [cell.rowIdx], [cell.colIdx]]; + + const newState = reducer(state, action); + expect(newState).toEqual({ ...state, isSelectMultiValues: false }); + expect(state.api.select).toHaveBeenCalledWith({ method: 'selectHyperCubeCells', params }); + }); + + it('should not call select when type is selectMultiValues but isSelectMultiValues is false', () => { + const action = { type: SelectionActions.SELECT_MULTI_VALUES } as SelectMultiValuesAction; + state.isSelectMultiValues = false; + + const newState = reducer(state, action); + expect(newState).toEqual({ ...state, isSelectMultiValues: false }); + expect(state.api.select).not.toHaveBeenCalled(); + }); + + it('should return state updated when the app is not in selection modal state when action.type is reset', () => { + const action = { type: SelectionActions.RESET } as ResetAction; + const newState = reducer(state, action); + expect(newState).toEqual({ ...state, rows: {}, colIdx: -1 }); + }); + + it('should return state updated with rows when action.type is clear', () => { + const action = { type: SelectionActions.CLEAR } as ClearAction; + const newState = reducer(state, action); + expect(newState).toEqual({ ...state, rows: {} }); + }); + + it('should return state unchanged when the app is in selection modal state and action.type is reset', () => { + const action = { type: SelectionActions.RESET } as ResetAction; + state.api.isModal = () => true; + const newState = reducer(state, action); + expect(newState).toEqual(state); + }); + }); + }); + + describe('handleAnnounceSelectionStatus', () => { + let announce: AnnounceFn; + let rowsLength: number; + let isAddition: boolean; + + beforeEach(() => { + announce = jest.fn(); + rowsLength = 1; + isAddition = true; + }); + + afterEach(() => jest.clearAllMocks()); + + it('should announce selected value and one selected value when rowsLength is 1 and isAddition is true', () => { + handleAnnounceSelectionStatus(announce, rowsLength, isAddition); + + expect(announce).toHaveBeenCalledWith({ + keys: ['SNTable.SelectionLabel.SelectedValue', 'SNTable.SelectionLabel.OneSelectedValue'], + }); + }); + + it('should announce selected value and two selected values when rowsLength is 2 and isAddition is true', () => { + rowsLength = 2; + handleAnnounceSelectionStatus(announce, rowsLength, isAddition); + + expect(announce).toHaveBeenCalledWith({ + keys: [ + 'SNTable.SelectionLabel.SelectedValue', + ['SNTable.SelectionLabel.SelectedValues', rowsLength.toString()], + ], + }); + }); + + it('should announce deselected value and one selected value when rowsLength is 1 and isAddition is false', () => { + isAddition = false; + handleAnnounceSelectionStatus(announce, rowsLength, isAddition); + + expect(announce).toHaveBeenCalledWith({ + keys: ['SNTable.SelectionLabel.DeselectedValue', 'SNTable.SelectionLabel.OneSelectedValue'], + }); + }); + + it('should announce deselected value and two selected values when rowsLength is 2 and isAddition is false', () => { + rowsLength = 2; + isAddition = false; + handleAnnounceSelectionStatus(announce, rowsLength, isAddition); + + expect(announce).toHaveBeenCalledWith({ + keys: [ + 'SNTable.SelectionLabel.DeselectedValue', + ['SNTable.SelectionLabel.SelectedValues', rowsLength.toString()], + ], + }); + }); + + it('should announce deselected value and exited selection mode when you have deselected the last value', () => { + rowsLength = 0; + isAddition = false; + handleAnnounceSelectionStatus(announce, rowsLength, isAddition); + + expect(announce).toHaveBeenCalledWith({ keys: ['SNTable.SelectionLabel.ExitedSelectionMode'] }); + }); + }); + + describe('getSelectedRows', () => { + let selectedRows: Record; + let cell: TableCell; + let evt: React.KeyboardEvent; + + beforeEach(() => { + selectedRows = { 1: 1 }; + cell = { + qElemNumber: 0, + rowIdx: 0, + } as TableCell; + evt = {} as React.KeyboardEvent; + }); + + it('should return array with only the last clicked item when ctrlKey is pressed', () => { + evt.ctrlKey = true; + + const updatedSelectedRows = getSelectedRows(selectedRows, cell, evt); + expect(updatedSelectedRows).toEqual({ [cell.qElemNumber]: cell.rowIdx }); + }); + + it('should return array with only the last clicked item metaKey cm is pressed', () => { + evt.metaKey = true; + + const updatedSelectedRows = getSelectedRows(selectedRows, cell, evt); + expect(updatedSelectedRows).toEqual({ [cell.qElemNumber]: cell.rowIdx }); + }); + + it('should return array with selected item removed if it already was in selectedRows', () => { + cell.qElemNumber = 1; + cell.rowIdx = 1; + + const updatedSelectedRows = getSelectedRows(selectedRows, cell, evt); + expect(updatedSelectedRows).toEqual({}); + }); + + it('should return array with selected item added if it was not in selectedRows before', () => { + const updatedSelectedRows = getSelectedRows(selectedRows, cell, evt); + expect(updatedSelectedRows).toEqual({ 1: 1, [cell.qElemNumber]: cell.rowIdx }); + }); + + it('should add the current cell and the next cell to selectedRows when press shift and arrow down key', () => { + evt.shiftKey = true; + evt.key = 'ArrowDown'; + cell.nextQElemNumber = 2; + + const updatedSelectedRows = getSelectedRows(selectedRows, cell, evt); + expect(updatedSelectedRows).toEqual({ 1: 1, [cell.qElemNumber]: cell.rowIdx, [cell.nextQElemNumber]: 1 }); + }); + }); + + describe('getCellSelectionState', () => { + let isModal: boolean; + let cell: TableCell; + let value: ContextValue; + + beforeEach(() => { + isModal = true; + cell = { + qElemNumber: 1, + colIdx: 1, + } as TableCell; + value = { + selectionState: { + colIdx: 1, + rows: { 1: 1 }, + api: { + isModal: () => isModal, + } as ExtendedSelectionAPI, + isSelectMultiValues: false, + }, + } as unknown as ContextValue; + }); + + afterEach(() => jest.clearAllMocks()); + + it('should return selected when selected', () => { + const cellState = getCellSelectionState(cell, value); + expect(cellState).toEqual(SelectionStates.SELECTED); + }); + + it('should return possible when row is not selected', () => { + cell.qElemNumber = 2; + + const cellState = getCellSelectionState(cell, value); + expect(cellState).toEqual(SelectionStates.POSSIBLE); + }); + + it('should return excluded when colIdx is not in selectionState', () => { + cell.colIdx = 2; + + const cellState = getCellSelectionState(cell, value); + expect(cellState).toEqual(SelectionStates.EXCLUDED); + }); + + it('should return inactive when when isModal is false', () => { + value.selectionState.colIdx = -1; + value.selectionState.rows = {}; + isModal = false; + + const cellState = getCellSelectionState(cell, value); + expect(cellState).toEqual(SelectionStates.INACTIVE); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/utils/__tests__/styling-utils.spec.js b/prebuilt/react-native-sn-table/table/utils/__tests__/styling-utils.spec.js new file mode 100644 index 00000000..b02e5dcb --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/__tests__/styling-utils.spec.js @@ -0,0 +1,438 @@ +import { + STYLING_DEFAULTS, + SELECTION_STYLING, + getColor, + getBaseStyling, + getHeaderStyle, + getBodyCellStyle, + getColumnStyle, + getSelectionStyle, +} from '../styling-utils'; +import { SelectionStates } from '../selections-utils'; + +describe('styling-utils', () => { + let resolvedColor; + let altResolvedColor; + const theme = { + // very simple mock of getColorPickerColor. Normally color.color has to be null for the fn to return null + getColorPickerColor: ({ index, color }) => { + switch (index) { + case -1: + return color; + case 0: + return 'none'; + case 1: + return resolvedColor; + case 2: + return altResolvedColor; + default: + return null; + } + }, + getStyle: () => {}, + table: { + body: { borderColor: '#D9D9D9' }, + backgroundColor: '#323232', + }, + }; + + describe('getColor', () => { + let color; + let defaultColor; + + beforeEach(() => { + defaultColor = '#000'; + color = { + index: -1, + color: null, + }; + resolvedColor = '#fff'; + }); + + it('should return a hex color when color obj has index', () => { + color.index = 1; + + const resultColor = getColor(defaultColor, theme, color); + expect(resultColor).toBe(resolvedColor); + }); + it('should return a default color when getColorPickerColor returns false', () => { + const resultColor = getColor(defaultColor, theme, color); + expect(resultColor).toBe(defaultColor); + }); + + it('should return a default color when getColorPickerColor returns none', () => { + // Some palettes have none as the first value in the array of colors + color.index = 0; + + const resultColor = getColor(defaultColor, theme, color); + expect(resultColor).toBe(defaultColor); + }); + + it('should return a default color when color is undefined', () => { + color = undefined; + + const resultColor = getColor(defaultColor, theme, color); + expect(resultColor).toBe(defaultColor); + }); + }); + + describe('getBaseStyling', () => { + let styleObj; + let objetName; + + beforeEach(() => { + resolvedColor = '#fff'; + styleObj = { + fontColor: { + index: 1, + color: null, + }, + fontSize: 12, + }; + objetName = ''; + }); + + it('should return styling with fontColor, fontSize and padding', () => { + const resultStyling = getBaseStyling(styleObj, objetName, theme); + expect(resultStyling).toEqual({ + borderColor: '#D9D9D9', + borderStyle: 'solid', + color: '#fff', + fontSize: 12, + padding: '6px 12px', + }); + }); + it('should return styling with fontSize and padding', () => { + styleObj.fontColor = null; + const resultStyling = getBaseStyling(styleObj, objetName, theme); + expect(resultStyling).toEqual({ + borderColor: '#D9D9D9', + borderStyle: 'solid', + fontSize: 12, + padding: '6px 12px', + }); + }); + it('should return styling with fontSize and padding when the index for font color is -1 and color is null', () => { + styleObj.fontColor = { index: -1, color: null }; + + const resultStyling = getBaseStyling(styleObj, objetName, theme); + expect(resultStyling).toEqual({ + borderColor: '#D9D9D9', + borderStyle: 'solid', + fontSize: 12, + padding: '6px 12px', + }); + }); + it('should return styling with fontSize, padding and font color when the index for font color is -1 and color is null and there is a color from theme', () => { + styleObj.fontColor = { index: -1, color: null }; + const customTheme = { + ...theme, + getStyle: () => '#111', + }; + + const resultStyling = getBaseStyling(styleObj, objetName, customTheme); + expect(resultStyling).toEqual({ + borderColor: '#D9D9D9', + borderStyle: 'solid', + color: '#111', + fontSize: 12, + padding: '6px 12px', + fontFamily: '#111', + }); + }); + it('should return styling with fontSize, padding and font color when the index for font color is -1 and the color is not null', () => { + styleObj.fontColor = { index: -1, color: 'fff' }; + + const resultStyling = getBaseStyling(styleObj, objetName, theme); + expect(resultStyling).toEqual({ + borderColor: '#D9D9D9', + borderStyle: 'solid', + color: 'fff', + fontSize: 12, + padding: '6px 12px', + }); + }); + it('should return styling with fontColor as the font size and padding are from sprout theme', () => { + styleObj.fontSize = null; + + const resultStyling = getBaseStyling(styleObj, objetName, theme); + expect(resultStyling).toEqual({ + color: '#fff', + borderColor: '#D9D9D9', + borderStyle: 'solid', + }); + }); + it('should return styling with custom padding', () => { + styleObj.padding = '4px'; + + const resultStyling = getBaseStyling(styleObj, objetName, theme); + expect(resultStyling.padding).toBe('4px'); + }); + }); + + describe('getHeaderStyle', () => { + let layout; + + beforeEach(() => { + layout = { + components: [ + { + header: { + fontColor: '#444444', + fontSize: 44, + }, + }, + ], + }; + }); + + it('should return empty object except backgroundColor and border as the padding and font size are from sprout theme', () => { + layout = {}; + + const resultStyling = getHeaderStyle(layout, theme); + expect(resultStyling).toEqual({ + backgroundColor: '#323232', + borderColor: '#D9D9D9', + borderStyle: 'solid', + borderWidth: '1px 1px 1px 0px', + cursor: 'pointer', + sortLabelColor: 'rgba(255,255,255,0.9)', + }); + }); + it('should return header style with only fontColor except backgroundColor and border', () => { + layout = { + components: [ + { + header: { + fontColor: '#444444', + }, + }, + ], + }; + + const resultStyling = getHeaderStyle(layout, theme); + expect(resultStyling).toEqual({ + color: '#404040', + cursor: 'pointer', + backgroundColor: '#323232', + borderColor: '#D9D9D9', + borderStyle: 'solid', + borderWidth: '1px 1px 1px 0px', + sortLabelColor: '#404040', + }); + }); + it('should return all header style from layout', () => { + const resultStyling = getHeaderStyle(layout, theme); + expect(resultStyling).toEqual({ + color: '#404040', + cursor: 'pointer', + fontSize: 44, + padding: '22px 44px', + backgroundColor: '#323232', + borderColor: '#D9D9D9', + borderStyle: 'solid', + borderWidth: '1px 1px 1px 0px', + sortLabelColor: '#404040', + }); + }); + }); + + describe('getBodyCellStyle', () => { + let layout; + + beforeEach(() => { + resolvedColor = '#222222'; // dark color + altResolvedColor = '#dddddd'; // light color + + layout = { + components: [ + { + content: { + fontSize: 22, + fontColor: { + index: 1, + color: null, + }, + hoverColor: { + index: -1, + color: null, + }, + hoverFontColor: { + index: -1, + color: null, + }, + }, + }, + ], + }; + }); + + it('should return styling with default hoverBackgroundColor and hoverFontColor', () => { + layout = {}; + + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling).toEqual({ + hoverFontColor: '', + borderColor: '#D9D9D9', + hoverBackgroundColor: '#f4f4f4', + borderStyle: 'solid', + borderWidth: '0px 1px 1px 0px', + }); + }); + it('should return styling with fontColor, fontSize, padding plus default hoverBackgroundColor and hoverFontColor', () => { + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling).toEqual({ + fontSize: 22, + color: '#222222', + padding: '11px 22px', + hoverBackgroundColor: '#f4f4f4', + hoverFontColor: '', + borderColor: '#D9D9D9', + borderStyle: 'solid', + borderWidth: '0px 1px 1px 0px', + }); + }); + // Only checking hover properties from here on + it('should return styling with no hoverBackgroundColor and the specified hoverFontColor', () => { + layout.components[0].content.hoverFontColor.index = 1; + + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling.hoverBackgroundColor).toBe(''); + expect(resultStyling.hoverFontColor).toBe(resolvedColor); + }); + it('should return styling with dark hoverBackgroundColor from theme and the white hoverFontColor', () => { + theme.getStyle = () => '#111'; + + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling.hoverBackgroundColor).toBe('#111'); + expect(resultStyling.hoverFontColor).toBe(STYLING_DEFAULTS.WHITE); + }); + it('should return styling with light hoverBackgroundColor from theme and the default hoverFontColor', () => { + theme.getStyle = () => '#fff'; + + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling.hoverBackgroundColor).toBe('#fff'); + expect(resultStyling.hoverFontColor).toBe(STYLING_DEFAULTS.FONT_COLOR); + }); + it('should return styling with dark hoverBackgroundColor and white hoverFontColor', () => { + layout.components[0].content.hoverColor.index = 1; + + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling.hoverBackgroundColor).toBe(resolvedColor); + expect(resultStyling.hoverFontColor).toBe(STYLING_DEFAULTS.WHITE); + }); + it('should return styling with light hoverBackgroundColor and the default hoverFontColor', () => { + layout.components[0].content.hoverColor.index = 2; + + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling.hoverBackgroundColor).toBe(altResolvedColor); + expect(resultStyling.hoverFontColor).toBe(STYLING_DEFAULTS.FONT_COLOR); + }); + it('should return styling with set hoverBackgroundColor and hoverFontColor', () => { + layout.components[0].content.hoverColor.index = 1; + layout.components[0].content.hoverFontColor.index = 2; + + const resultStyling = getBodyCellStyle(layout, theme); + expect(resultStyling.hoverBackgroundColor).toBe(resolvedColor); + expect(resultStyling.hoverFontColor).toBe(altResolvedColor); + }); + }); + + describe('getColumnStyle', () => { + let styling; + let qAttrExps; + let stylingInfo; + + beforeEach(() => { + styling = { color: 'someFontColor' }; + qAttrExps = { + qValues: [{ qText: '#dddddd' }, { qText: '#111111' }], + }; + stylingInfo = ['cellBackgroundColor', 'cellForegroundColor']; + }); + + it('should return styling with both new fontColor and backgroundColor when selected', () => { + const columnStyle = getColumnStyle(styling, qAttrExps, stylingInfo); + expect(columnStyle.backgroundColor).toBe('rgb(221,221,221)'); + expect(columnStyle.color).toBe('rgb(17,17,17)'); + }); + it('should return styling with new fontColor', () => { + qAttrExps.qValues = [qAttrExps.qValues[1]]; + stylingInfo = [stylingInfo[1]]; + + const columnStyle = getColumnStyle(styling, qAttrExps, stylingInfo); + expect(columnStyle.backgroundColor).toBe(undefined); + expect(columnStyle.color).toBe('rgb(17,17,17)'); + }); + it('should return styling with backgroundColor', () => { + qAttrExps.qValues = [qAttrExps.qValues[0]]; + stylingInfo = [stylingInfo[0]]; + + const columnStyle = getColumnStyle(styling, qAttrExps, stylingInfo); + expect(columnStyle.backgroundColor).toBe('rgb(221,221,221)'); + expect(columnStyle.color).toBe(STYLING_DEFAULTS.FONT_COLOR); + }); + it('should return styling unchanged', () => { + qAttrExps = undefined; + stylingInfo = []; + + const columnStyle = getColumnStyle(styling, qAttrExps, stylingInfo); + expect(columnStyle.backgroundColor).toBe(undefined); + expect(columnStyle.color).toBe('someFontColor'); + }); + }); + + describe('getSelectionStyle', () => { + let styling; + let cellSelectionState; + let themeBackgroundColor; + + beforeEach(() => { + styling = { + otherStyling: 'otherStyling', + backgroundColor: '#123456', + }; + cellSelectionState = SelectionStates.SELECTED; + }); + + it('should return selected when selected styling', () => { + const selectionStyling = getSelectionStyle(styling, cellSelectionState, themeBackgroundColor); + expect(selectionStyling).toEqual({ ...styling, ...SELECTION_STYLING.SELECTED }); + }); + + it('should return excluded styling when other column', () => { + cellSelectionState = SelectionStates.EXCLUDED; + + const selectionStyling = getSelectionStyle(styling, cellSelectionState); + expect(selectionStyling).toEqual({ + ...styling, + background: `${STYLING_DEFAULTS.EXCLUDED_BACKGROUND}, ${styling.backgroundColor}`, + }); + }); + + it('should return excluded styling with columns background when other column and background color exists', () => { + cellSelectionState = SelectionStates.EXCLUDED; + styling.backgroundColor = 'someColor'; + + const selectionStyling = getSelectionStyle(styling, cellSelectionState, themeBackgroundColor); + expect(selectionStyling).toEqual({ + ...styling, + background: `${STYLING_DEFAULTS.EXCLUDED_BACKGROUND}, ${styling.backgroundColor}`, + }); + }); + + it('should return possible styling when active and available to select', () => { + cellSelectionState = SelectionStates.POSSIBLE; + + const selectionStyling = getSelectionStyle(styling, cellSelectionState, themeBackgroundColor); + expect(selectionStyling).toEqual({ ...styling, ...SELECTION_STYLING.POSSIBLE }); + }); + + it('should return empty object when no active selections', () => { + cellSelectionState = SelectionStates.INACTIVE; + + const selectionStyling = getSelectionStyle(styling, cellSelectionState, themeBackgroundColor); + expect(selectionStyling).toEqual(styling); + }); + }); +}); diff --git a/prebuilt/react-native-sn-table/table/utils/color-utils.js b/prebuilt/react-native-sn-table/table/utils/color-utils.js new file mode 100644 index 00000000..5fa2c185 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/color-utils.js @@ -0,0 +1,71 @@ +/* eslint-disable no-cond-assign */ +import cssColors from './css-colors'; + +const getChunksFromString = (st, chunkSize) => st.match(new RegExp(`.{${chunkSize}}`, 'g')); +const convertHexUnitTo256 = (hexStr) => parseInt(hexStr.repeat(2 / hexStr.length), 16); +const hexToRGBAorRGB = (hex) => { + const chunkSize = Math.floor((hex.length - 1) / 3); + const hexArr = getChunksFromString(hex.slice(1), chunkSize); + const [r, g, b, a] = hexArr.map(convertHexUnitTo256); + return typeof a !== 'undefined' ? `rgba(${r},${g},${b},${a / 255})` : `rgb(${r},${g},${b})`; +}; + +export function resolveToRGBAorRGB(input) { + // rgb + let matches = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(input); + if (matches) { + return `rgb(${matches[1]},${matches[2]},${matches[3]})`; + } + // rgba + matches = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d(\.\d+)?)\s*\)$/i.exec(input); + if (matches) { + return `rgba(${matches[1]},${matches[2]},${matches[3]},${matches[4]})`; + } + // argb + matches = /^argb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(input); + if (matches) { + const a = Math.round(matches[1] / 2.55) / 100; + return `rgba(${matches[2]},${matches[3]},${matches[4]},${a})`; + } + // hex (#rgb, #rgba, #rrggbb, and #rrggbbaa) + matches = /^#(?:(?:[\da-f]{3}){1,2}|(?:[\da-f]{4}){1,2})$/i.exec(input); + if (matches) { + return hexToRGBAorRGB(input); + } + // css color + const color = input && cssColors[input.toLowerCase()]; + if (color) { + return typeof color.a !== 'undefined' + ? `rgba(${color.r},${color.g},${color.b},${color.a})` + : `rgb(${color.r},${color.g},${color.b})`; + } + // invalid + return 'none'; +} + +export function isDarkColor(color) { + const rgba = resolveToRGBAorRGB(color); + const matches = + /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.exec(rgba) || + /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d(\.\d+)?)\s*\)$/i.exec(rgba); + const r = +matches?.[1]; + const g = +matches?.[2]; + const b = +matches?.[3]; + + // Using the HSP (Highly Sensitive Poo) value, determine whether the color is light or dark + // HSP < 125, the color is dark, otherwise, the color is light + return 0.299 * r + 0.587 * g + 0.114 * b < 125; +} + +export function isTransparentColor(color) { + const rgba = resolveToRGBAorRGB(color); + const matches = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d(\.\d+)?)\s*\)$/i.exec(rgba); + return +matches?.[4] === 0; +} + +export function removeOpacity(color) { + const rgba = resolveToRGBAorRGB(color); + const matches = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d(\.\d+)?)\s*\)$/i.exec(rgba); + if (matches) return `rgb(${matches[1]},${matches[2]},${matches[3]})`; + return color; +} diff --git a/prebuilt/react-native-sn-table/table/utils/css-colors.js b/prebuilt/react-native-sn-table/table/utils/css-colors.js new file mode 100644 index 00000000..6b7617c1 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/css-colors.js @@ -0,0 +1,153 @@ +const cssColors = { + aliceblue: { r: 240, g: 248, b: 255 }, + antiquewhite: { r: 250, g: 235, b: 215 }, + aqua: { r: 0, g: 255, b: 255 }, + aquamarine: { r: 127, g: 255, b: 212 }, + azure: { r: 240, g: 255, b: 255 }, + beige: { r: 245, g: 245, b: 220 }, + bisque: { r: 255, g: 228, b: 196 }, + black: { r: 0, g: 0, b: 0 }, + blanchedalmond: { r: 255, g: 235, b: 205 }, + blue: { r: 0, g: 0, b: 255 }, + blueviolet: { r: 138, g: 43, b: 226 }, + brown: { r: 165, g: 42, b: 42 }, + burlywood: { r: 222, g: 184, b: 135 }, + cadetblue: { r: 95, g: 158, b: 160 }, + chartreuse: { r: 127, g: 255, b: 0 }, + chocolate: { r: 210, g: 105, b: 30 }, + coral: { r: 255, g: 127, b: 80 }, + cornflowerblue: { r: 100, g: 149, b: 237 }, + cornsilk: { r: 255, g: 248, b: 220 }, + crimson: { r: 220, g: 20, b: 60 }, + cyan: { r: 0, g: 255, b: 255 }, + darkblue: { r: 0, g: 0, b: 139 }, + darkcyan: { r: 0, g: 139, b: 139 }, + darkgoldenrod: { r: 184, g: 134, b: 11 }, + darkgray: { r: 169, g: 169, b: 169 }, + darkgreen: { r: 0, g: 100, b: 0 }, + darkgrey: { r: 169, g: 169, b: 169 }, + darkkhaki: { r: 189, g: 183, b: 107 }, + darkmagenta: { r: 139, g: 0, b: 139 }, + darkolivegreen: { r: 85, g: 107, b: 47 }, + darkorange: { r: 255, g: 140, b: 0 }, + darkorchid: { r: 153, g: 50, b: 204 }, + darkred: { r: 139, g: 0, b: 0 }, + darksalmon: { r: 233, g: 150, b: 122 }, + darkseagreen: { r: 143, g: 188, b: 143 }, + darkslateblue: { r: 72, g: 61, b: 139 }, + darkslategray: { r: 47, g: 79, b: 79 }, + darkslategrey: { r: 47, g: 79, b: 79 }, + darkturquoise: { r: 0, g: 206, b: 209 }, + darkviolet: { r: 148, g: 0, b: 211 }, + deeppink: { r: 255, g: 20, b: 147 }, + deepskyblue: { r: 0, g: 191, b: 255 }, + dimgray: { r: 105, g: 105, b: 105 }, + dimgrey: { r: 105, g: 105, b: 105 }, + dodgerblue: { r: 30, g: 144, b: 255 }, + firebrick: { r: 178, g: 34, b: 34 }, + floralwhite: { r: 255, g: 250, b: 240 }, + forestgreen: { r: 34, g: 139, b: 34 }, + fuchsia: { r: 255, g: 0, b: 255 }, + gainsboro: { r: 220, g: 220, b: 220 }, + ghostwhite: { r: 248, g: 248, b: 255 }, + gold: { r: 255, g: 215, b: 0 }, + goldenrod: { r: 218, g: 165, b: 32 }, + gray: { r: 128, g: 128, b: 128 }, + green: { r: 0, g: 128, b: 0 }, + greenyellow: { r: 173, g: 255, b: 47 }, + grey: { r: 128, g: 128, b: 128 }, + honeydew: { r: 240, g: 255, b: 240 }, + hotpink: { r: 255, g: 105, b: 180 }, + indianred: { r: 205, g: 92, b: 92 }, + indigo: { r: 75, g: 0, b: 130 }, + ivory: { r: 255, g: 255, b: 240 }, + khaki: { r: 240, g: 230, b: 140 }, + lavender: { r: 230, g: 230, b: 250 }, + lavenderblush: { r: 255, g: 240, b: 245 }, + lawngreen: { r: 124, g: 252, b: 0 }, + lemonchiffon: { r: 255, g: 250, b: 205 }, + lightblue: { r: 173, g: 216, b: 230 }, + lightcoral: { r: 240, g: 128, b: 128 }, + lightcyan: { r: 224, g: 255, b: 255 }, + lightgoldenrodyellow: { r: 250, g: 250, b: 210 }, + lightgray: { r: 211, g: 211, b: 211 }, + lightgreen: { r: 144, g: 238, b: 144 }, + lightgrey: { r: 211, g: 211, b: 211 }, + lightpink: { r: 255, g: 182, b: 193 }, + lightsalmon: { r: 255, g: 160, b: 122 }, + lightseagreen: { r: 32, g: 178, b: 170 }, + lightskyblue: { r: 135, g: 206, b: 250 }, + lightslategray: { r: 119, g: 136, b: 153 }, + lightslategrey: { r: 119, g: 136, b: 153 }, + lightsteelblue: { r: 176, g: 196, b: 222 }, + lightyellow: { r: 255, g: 255, b: 224 }, + lime: { r: 0, g: 255, b: 0 }, + limegreen: { r: 50, g: 205, b: 50 }, + linen: { r: 250, g: 240, b: 230 }, + magenta: { r: 255, g: 0, b: 255 }, + maroon: { r: 128, g: 0, b: 0 }, + mediumaquamarine: { r: 102, g: 205, b: 170 }, + mediumblue: { r: 0, g: 0, b: 205 }, + mediumorchid: { r: 186, g: 85, b: 211 }, + mediumpurple: { r: 147, g: 112, b: 219 }, + mediumseagreen: { r: 60, g: 179, b: 113 }, + mediumslateblue: { r: 123, g: 104, b: 238 }, + mediumspringgreen: { r: 0, g: 250, b: 154 }, + mediumturquoise: { r: 72, g: 209, b: 204 }, + mediumvioletred: { r: 199, g: 21, b: 133 }, + midnightblue: { r: 25, g: 25, b: 112 }, + mintcream: { r: 245, g: 255, b: 250 }, + mistyrose: { r: 255, g: 228, b: 225 }, + moccasin: { r: 255, g: 228, b: 181 }, + navajowhite: { r: 255, g: 222, b: 173 }, + navy: { r: 0, g: 0, b: 128 }, + oldlace: { r: 253, g: 245, b: 230 }, + olive: { r: 128, g: 128, b: 0 }, + olivedrab: { r: 107, g: 142, b: 35 }, + orange: { r: 255, g: 165, b: 0 }, + orangered: { r: 255, g: 69, b: 0 }, + orchid: { r: 218, g: 112, b: 214 }, + palegoldenrod: { r: 238, g: 232, b: 170 }, + palegreen: { r: 152, g: 251, b: 152 }, + paleturquoise: { r: 175, g: 238, b: 238 }, + palevioletred: { r: 219, g: 112, b: 147 }, + papayawhip: { r: 255, g: 239, b: 213 }, + peachpuff: { r: 255, g: 218, b: 185 }, + peru: { r: 205, g: 133, b: 63 }, + pink: { r: 255, g: 192, b: 203 }, + plum: { r: 221, g: 160, b: 221 }, + powderblue: { r: 176, g: 224, b: 230 }, + purple: { r: 128, g: 0, b: 128 }, + rebeccapurple: { r: 102, g: 51, b: 153 }, + red: { r: 255, g: 0, b: 0 }, + rosybrown: { r: 188, g: 143, b: 143 }, + royalblue: { r: 65, g: 105, b: 225 }, + saddlebrown: { r: 139, g: 69, b: 19 }, + salmon: { r: 250, g: 128, b: 114 }, + sandybrown: { r: 244, g: 164, b: 96 }, + seagreen: { r: 46, g: 139, b: 87 }, + seashell: { r: 255, g: 245, b: 238 }, + sienna: { r: 160, g: 82, b: 45 }, + silver: { r: 192, g: 192, b: 192 }, + skyblue: { r: 135, g: 206, b: 235 }, + slateblue: { r: 106, g: 90, b: 205 }, + slategray: { r: 112, g: 128, b: 144 }, + slategrey: { r: 112, g: 128, b: 144 }, + snow: { r: 255, g: 250, b: 250 }, + springgreen: { r: 0, g: 255, b: 127 }, + steelblue: { r: 70, g: 130, b: 180 }, + tan: { r: 210, g: 180, b: 140 }, + teal: { r: 0, g: 128, b: 128 }, + thistle: { r: 216, g: 191, b: 216 }, + tomato: { r: 255, g: 99, b: 71 }, + transparent: { r: 255, g: 255, b: 255, a: 0 }, + turquoise: { r: 64, g: 224, b: 208 }, + violet: { r: 238, g: 130, b: 238 }, + wheat: { r: 245, g: 222, b: 179 }, + white: { r: 255, g: 255, b: 255 }, + whitesmoke: { r: 245, g: 245, b: 245 }, + yellow: { r: 255, g: 255, b: 0 }, + yellowgreen: { r: 154, g: 205, b: 50 }, +}; + +export default cssColors; diff --git a/prebuilt/react-native-sn-table/table/utils/get-cell-renderer.ts b/prebuilt/react-native-sn-table/table/utils/get-cell-renderer.ts new file mode 100644 index 00000000..d9c6330a --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/get-cell-renderer.ts @@ -0,0 +1,12 @@ +import TableCell from '@mui/material/TableCell'; +import withColumnStyling from '../components/withColumnStyling'; +import withSelections from '../components/withSelections'; +import withStyling from '../components/withStyling'; + +export default function getCellRenderer(hasColumnStyling: boolean, isSelectionsEnabled: boolean) { + // withStyling always runs last, applying whatever styling it gets + let cell = withStyling(TableCell); + if (isSelectionsEnabled) cell = withSelections(cell); + if (hasColumnStyling) cell = withColumnStyling(cell); + return cell; +} diff --git a/prebuilt/react-native-sn-table/table/utils/handle-accessibility.js b/prebuilt/react-native-sn-table/table/utils/handle-accessibility.js new file mode 100644 index 00000000..49b46059 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/handle-accessibility.js @@ -0,0 +1,112 @@ +export const getCellElement = (rootElement, cellCoord) => + rootElement.getElementsByClassName('sn-table-row')[cellCoord[0]]?.getElementsByClassName('sn-table-cell')[ + cellCoord[1] + ]; + +export const findCellWithTabStop = (rootElement) => rootElement.querySelector("td[tabindex='0'], th[tabindex='0']"); + +export const updateFocus = ({ focusType, cell }) => { + if (!cell) return; + + switch (focusType) { + case 'focus': + cell.focus(); + cell.setAttribute('tabIndex', '0'); + break; + case 'blur': + cell.blur(); + cell.setAttribute('tabIndex', '-1'); + break; + case 'addTab': + cell.setAttribute('tabIndex', '0'); + break; + case 'removeTab': + cell.setAttribute('tabIndex', '-1'); + break; + default: + break; + } +}; + +export const removeAndFocus = (newCoord, rootElement, setFocusedCellCoord, keyboard) => { + updateFocus({ focusType: 'removeTab', cell: findCellWithTabStop(rootElement) }); + setFocusedCellCoord(newCoord); + keyboard.enabled && keyboard.focus(); +}; + +export const handleClickToFocusBody = (cell, rootElement, setFocusedCellCoord, keyboard, totalsPosition) => { + const { rawRowIdx, rawColIdx } = cell; + const adjustedRowIdx = totalsPosition === 'top' ? rawRowIdx + 2 : rawRowIdx + 1; + removeAndFocus([adjustedRowIdx, rawColIdx], rootElement, setFocusedCellCoord, keyboard); +}; + +export const handleClickToFocusHead = (columnIndex, rootElement, setFocusedCellCoord, keyboard) => { + removeAndFocus([0, columnIndex], rootElement, setFocusedCellCoord, keyboard); +}; + +export const handleMouseDownLabelToFocusHeadCell = (evt, rootElement, columnIndex) => { + evt.preventDefault(); + updateFocus({ focusType: 'focus', cell: getCellElement(rootElement, [0, columnIndex]) }); +}; + +export const handleResetFocus = ({ + focusedCellCoord, + rootElement, + shouldRefocus, + isSelectionMode, + setFocusedCellCoord, + keyboard, + announce, +}) => { + updateFocus({ focusType: 'removeTab', cell: findCellWithTabStop(rootElement) }); + // If you have selections ongoing, you want to stay on the same column + const cellCoord = isSelectionMode ? [1, focusedCellCoord[1]] : [0, 0]; + if (!keyboard.enabled || keyboard.active) { + // Only run this if updates come from inside table + const focusType = shouldRefocus.current ? 'focus' : 'addTab'; + shouldRefocus.current = false; + const cell = getCellElement(rootElement, cellCoord); + updateFocus({ focusType, cell }); + + if (isSelectionMode) { + const hasSelectedClassName = cell?.classList?.contains('selected'); + announce({ + keys: [ + `${cell.textContent},`, + hasSelectedClassName ? 'SNTable.SelectionLabel.SelectedValue' : 'SNTable.SelectionLabel.NotSelectedValue', + ], + }); + } + } + setFocusedCellCoord(cellCoord); +}; + +export const handleFocusoutEvent = (evt, shouldRefocus, keyboard) => { + if (keyboard.enabled && !evt.currentTarget.contains(evt.relatedTarget) && !shouldRefocus.current) { + evt.currentTarget.querySelector('#sn-table-announcer--01').innerHTML = ''; + evt.currentTarget.querySelector('#sn-table-announcer--02').innerHTML = ''; + // Blur the table but not focus its parent element + // when keyboard.active is false, this has no effect + keyboard.blur(false); + } +}; + +export const focusSelectionToolbar = (element, keyboard, last) => { + const clientConfirmButton = element + .closest('.qv-object-wrapper') + ?.querySelector('.sel-toolbar-confirm')?.parentElement; + if (clientConfirmButton) { + clientConfirmButton.focus(); + return; + } + keyboard.focusSelection(last); +}; + +export const announceSelectionState = (announce, nextCell, isSelectionMode) => { + if (isSelectionMode) { + const hasActiveClassName = nextCell.classList.contains('selected'); + hasActiveClassName + ? announce({ keys: ['SNTable.SelectionLabel.SelectedValue'] }) + : announce({ keys: ['SNTable.SelectionLabel.NotSelectedValue'] }); + } +}; diff --git a/prebuilt/react-native-sn-table/table/utils/handle-key-press.js b/prebuilt/react-native-sn-table/table/utils/handle-key-press.js new file mode 100644 index 00000000..7e3cc61e --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/handle-key-press.js @@ -0,0 +1,244 @@ +import { updateFocus, focusSelectionToolbar, getCellElement, announceSelectionState } from './handle-accessibility'; +import { SelectionActions } from './selections-utils'; + +const isCtrlShift = (evt) => evt.shiftKey && (evt.ctrlKey || evt.metaKey); + +export const preventDefaultBehavior = (evt) => { + evt.stopPropagation(); + evt.preventDefault(); +}; + +export const handleTableWrapperKeyDown = ({ + evt, + totalRowCount, + page, + rowsPerPage, + handleChangePage, + setShouldRefocus, + keyboard, + isSelectionMode, +}) => { + if (isCtrlShift(evt)) { + preventDefaultBehavior(evt); + // ctrl + shift + left/right arrow keys: go to previous/next page + const lastPage = Math.ceil(totalRowCount / rowsPerPage) - 1; + if (evt.key === 'ArrowRight' && page < lastPage) { + setShouldRefocus(); + handleChangePage(page + 1); + } else if (evt.key === 'ArrowLeft' && page > 0) { + setShouldRefocus(); + handleChangePage(page - 1); + } + } else if (evt.key === 'Escape' && keyboard.enabled && !isSelectionMode) { + // escape key: tell Nebula to relinquish the table's focus to + // its parent element when nebula handles keyboard navigation + // and not in selection mode + preventDefaultBehavior(evt); + keyboard.blur(true); + } +}; + +export const arrowKeysNavigation = (evt, rowAndColumnCount, cellCoord, topAllowedRow = 0) => { + let [nextRow, nextCol] = cellCoord; + + switch (evt.key) { + case 'ArrowDown': + nextRow < rowAndColumnCount.rowCount - 1 && nextRow++; + break; + case 'ArrowUp': + nextRow > topAllowedRow && nextRow--; + break; + case 'ArrowRight': + // topAllowedRow greater than 0 means we are in selection mode + if (topAllowedRow > 0) break; + if (nextCol < rowAndColumnCount.columnCount - 1) { + nextCol++; + } else if (nextRow < rowAndColumnCount.rowCount - 1) { + nextRow++; + nextCol = 0; + } + break; + case 'ArrowLeft': + // topAllowedRow greater than 0 means we are in selection mode + if (topAllowedRow > 0) break; + if (nextCol > 0) { + nextCol--; + } else if (nextRow > 0) { + nextRow--; + nextCol = rowAndColumnCount.columnCount - 1; + } + break; + default: + break; + } + + return [nextRow, nextCol]; +}; + +export const getRowAndColumnCount = (rootElement) => { + const rowCount = rootElement.getElementsByClassName('sn-table-row').length; + const columnCount = rootElement.getElementsByClassName('sn-table-head-cell').length; + + return { rowCount, columnCount }; +}; + +export const moveFocus = (evt, rootElement, cellCoord, setFocusedCellCoord, topAllowedRow) => { + preventDefaultBehavior(evt); + evt.target.setAttribute('tabIndex', '-1'); + const rowAndColumnCount = getRowAndColumnCount(rootElement); + const nextCellCoord = arrowKeysNavigation(evt, rowAndColumnCount, cellCoord, topAllowedRow); + const nextCell = getCellElement(rootElement, nextCellCoord); + updateFocus({ focusType: 'focus', cell: nextCell }); + setFocusedCellCoord(nextCellCoord); + + return nextCell; +}; + +export const headHandleKeyPress = ({ + evt, + rootElement, + cellCoord, + column, + changeSortOrder, + layout, + isSortingEnabled, + setFocusedCellCoord, +}) => { + switch (evt.key) { + case 'ArrowDown': + case 'ArrowRight': + case 'ArrowLeft': + !isCtrlShift(evt) && moveFocus(evt, rootElement, cellCoord, setFocusedCellCoord); + break; + // Space bar / Enter: update the sorting + case ' ': + case 'Enter': + preventDefaultBehavior(evt); + isSortingEnabled && changeSortOrder(layout, column); + break; + default: + break; + } +}; + +/** + * Handle totals row key press + * + * @param {event} evt + * @param {Object} rootElement + * @param {Array} cellCoord + * @param {Function} setFocusedCellCoord + */ +export const totalHandleKeyPress = (evt, rootElement, cellCoord, setFocusedCellCoord) => { + switch (evt.key) { + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowRight': + case 'ArrowLeft': { + !isCtrlShift(evt) && moveFocus(evt, rootElement, cellCoord, setFocusedCellCoord); + break; + } + default: + break; + } +}; + +export const bodyHandleKeyPress = ({ + evt, + rootElement, + cell, + selectionDispatch, + isSelectionsEnabled, + setFocusedCellCoord, + announce, + keyboard, + paginationNeeded, + totalsPosition, + selectionsAPI, +}) => { + const isSelectionMode = selectionsAPI.isModal(); + // Adjust the cellCoord depending on the totals position + const firstBodyRowIdx = totalsPosition === 'top' ? 2 : 1; + const cellCoord = [cell.rawRowIdx + firstBodyRowIdx, cell.rawColIdx]; + + switch (evt.key) { + case 'ArrowUp': + case 'ArrowDown': { + // Make sure you can't navigate to header (and totals) in selection mode + const topAllowedRow = isSelectionMode ? firstBodyRowIdx : 0; + const nextCell = moveFocus(evt, rootElement, cellCoord, setFocusedCellCoord, topAllowedRow); + // Shift + up/down arrow keys: select multiple values + // When at the first/last row of the cell, shift + arrow up/down key, no value is selected + const isSelectMultiValues = + evt.shiftKey && + cell.isSelectable && + isSelectionsEnabled && + ((cell.prevQElemNumber !== undefined && evt.key === 'ArrowUp') || + (cell.nextQElemNumber !== undefined && evt.key === 'ArrowDown')); + if (isSelectMultiValues) { + selectionDispatch({ + type: SelectionActions.SELECT, + payload: { cell, evt, announce }, + }); + } else { + // When not selecting multiple we need to announce the selection state of the cell + announceSelectionState(announce, nextCell, isSelectionMode); + } + break; + } + case 'ArrowRight': + case 'ArrowLeft': + !isCtrlShift(evt) && moveFocus(evt, rootElement, cellCoord, setFocusedCellCoord); + break; + // Space bar: Selects value. + case ' ': + preventDefaultBehavior(evt); + cell.isSelectable && + isSelectionsEnabled && + selectionDispatch({ type: SelectionActions.SELECT, payload: { cell, evt, announce } }); + break; + // Enter: Confirms selections. + case 'Enter': + preventDefaultBehavior(evt); + if (isSelectionMode) { + selectionsAPI.confirm(); + announce({ keys: ['SNTable.SelectionLabel.SelectionsConfirmed'] }); + } + break; + // Esc: Cancels selections. If no selections, do nothing and handleTableWrapperKeyDown should catch it + case 'Escape': + if (isSelectionMode) { + preventDefaultBehavior(evt); + selectionsAPI.cancel(); + announce({ keys: ['SNTable.SelectionLabel.ExitedSelectionMode'] }); + } + break; + // Tab (+ shift): in selection mode and keyboard enabled, focus on selection toolbar + case 'Tab': + if (keyboard.enabled && isSelectionMode) { + if (evt.shiftKey) { + preventDefaultBehavior(evt); + focusSelectionToolbar(evt.target, keyboard, true); + } else if (!paginationNeeded) { + // Tab only: when there are no pagination controls, go tab straight to selection toolbar + preventDefaultBehavior(evt); + focusSelectionToolbar(evt.target, keyboard, false); + } + } + break; + default: + break; + } +}; + +export const bodyHandleKeyUp = (evt, selectionDispatch) => { + evt.key === 'Shift' && selectionDispatch({ type: SelectionActions.SELECT_MULTI_VALUES }); +}; + +export const handleLastTab = (evt, isSelectionMode, keyboard) => { + if (isSelectionMode && evt.key === 'Tab' && !evt.shiftKey) { + // tab key: focus on the selection toolbar + preventDefaultBehavior(evt); + focusSelectionToolbar(evt.target, keyboard, false); + } +}; diff --git a/prebuilt/react-native-sn-table/table/utils/handle-scroll.js b/prebuilt/react-native-sn-table/table/utils/handle-scroll.js new file mode 100644 index 00000000..ed216e0b --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/handle-scroll.js @@ -0,0 +1,53 @@ +export const handleHorizontalScroll = (evt, isRTL, memoedContainer) => { + if (evt.deltaX === 0) return; + + evt.stopPropagation(); + // scrollWidth is the width of an element's content, including content not visible on the screen due to overflow. + // offsetWidth is the element's CSS width, including any borders, padding, and vertical scrollbars + const maxScrollableWidth = memoedContainer.scrollWidth - memoedContainer.offsetWidth; + + // scrollLeft is the number of pixels scrolled from its left edge + // scrollLeft is 0 when the scrollbar is at its leftmost position + // (at the start of the scrolled content), + // and then increasingly negative as it is scrolled towards left. + let { scrollLeft } = memoedContainer; + + // evt.deltaX is the horizontal scroll amount + // evt.deltaX increasingly negative as you scroll towards left, + // increasingly positive as you scroll towards right + let scrolledDistance = scrollLeft + evt.deltaX; + + if (isRTL) scrolledDistance = maxScrollableWidth + scrolledDistance; + if (maxScrollableWidth > 0 && (scrolledDistance < 0 || scrolledDistance > maxScrollableWidth + 1)) { + evt.preventDefault(); + scrollLeft = isRTL + ? Math.min(0, Math.min(maxScrollableWidth, scrolledDistance)) + : Math.max(0, Math.min(maxScrollableWidth, scrolledDistance)); + } +}; + +export const handleNavigateTop = ({ tableContainerRef, focusedCellCoord, rootElement }) => { + const MIN_ROW_COUNT = 2; + + if (!tableContainerRef.current?.scrollTo) return; + + if (focusedCellCoord[0] < MIN_ROW_COUNT) { + tableContainerRef.current.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } else { + const [x, y] = focusedCellCoord; + const tableHead = rootElement.getElementsByClassName('sn-table-head-cell')[0]; + const rowElements = rootElement.getElementsByClassName('sn-table-row'); + const cell = rowElements[x]?.getElementsByClassName('sn-table-cell')[y]; + + if (cell.offsetTop - tableHead.offsetHeight - cell.offsetHeight <= tableContainerRef.current.scrollTop) { + const targetOffsetTop = tableContainerRef.current.scrollTop - cell.offsetHeight - tableHead.offsetHeight; + tableContainerRef.current.scrollTo({ + top: Math.max(0, targetOffsetTop), + behavior: 'smooth', + }); + } + } +}; diff --git a/prebuilt/react-native-sn-table/table/utils/scrolling-props.ts b/prebuilt/react-native-sn-table/table/utils/scrolling-props.ts new file mode 100644 index 00000000..0d1c301d --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/scrolling-props.ts @@ -0,0 +1,9 @@ +export type ScrollingProps = { + horizontal?: boolean; + keepFirstColumnInView?: boolean; + keepFirstColumnInViewTouch?: boolean; +}; + +export const freezeFirstColumn = ({ scrolling }: { scrolling: ScrollingProps }) => { + return scrolling?.keepFirstColumnInView; +}; diff --git a/prebuilt/react-native-sn-table/table/utils/selections-utils.ts b/prebuilt/react-native-sn-table/table/utils/selections-utils.ts new file mode 100644 index 00000000..5b935818 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/selections-utils.ts @@ -0,0 +1,198 @@ +// TODO: add this to global rules +/* eslint-disable @typescript-eslint/no-empty-interface */ +import { stardust } from '@nebula.js/stardust'; +import { TableCell, SelectionState, ExtendedSelectionAPI, ActionPayload, AnnounceFn, ContextValue } from '../../types'; + +export enum SelectionStates { + SELECTED = 'selected', + POSSIBLE = 'possible', + EXCLUDED = 'excluded', + INACTIVE = 'inactive', +} + +export enum SelectionActions { + SELECT = 'select', + RESET = 'reset', + CLEAR = 'clear', + SELECT_MULTI_VALUES = 'selectMultiValues', +} + +export interface Action { + type: T; +} + +export interface SelectAction extends Action { + payload: ActionPayload; +} +export interface SelectMultiValuesAction extends Action {} +export interface ResetAction extends Action {} +export interface ClearAction extends Action {} + +export type TSelectionActions = SelectAction | ResetAction | ClearAction | SelectMultiValuesAction; + +type AddSelectionListenersArgs = { + api: ExtendedSelectionAPI; + selectionDispatch: React.Dispatch; + setShouldRefocus(): void; + keyboard: stardust.Keyboard; + tableWrapperRef: React.MutableRefObject; +}; + +export function addSelectionListeners({ + api, + selectionDispatch, + setShouldRefocus, + keyboard, + tableWrapperRef, +}: AddSelectionListenersArgs) { + const resetSelections = () => { + selectionDispatch({ type: SelectionActions.RESET }); + }; + const clearSelections = () => { + selectionDispatch({ type: SelectionActions.CLEAR }); + }; + const resetSelectionsAndSetupRefocus = () => { + if (tableWrapperRef.current?.contains(document.activeElement)) { + // if there is a focus in the chart, + // set shouldRefocus so that you should either + // focus or just set the tabstop, after data has reloaded. + setShouldRefocus(); + } else if (keyboard.enabled) { + // if there is no focus on the chart, + // make sure you blur the table + // and focus the entire chart (table's parent element) + // @ts-ignore TODO: fix nebula api so that blur has the correct argument type + keyboard.blur?.(true); + } + resetSelections(); + }; + + api.on('deactivated', resetSelections); + api.on('canceled', resetSelections); + api.on('confirmed', resetSelectionsAndSetupRefocus); + api.on('cleared', clearSelections); + // Return function called on unmount + return () => { + api.removeListener('deactivated', resetSelections); + api.removeListener('canceled', resetSelections); + api.removeListener('confirmed', resetSelectionsAndSetupRefocus); + api.removeListener('cleared', clearSelections); + }; +} + +export const getCellSelectionState = (cell: TableCell, value: ContextValue): SelectionStates => { + const { + selectionState: { colIdx, rows, api }, + } = value; + let cellState = SelectionStates.INACTIVE; + if (api.isModal()) { + if (colIdx !== cell.colIdx) { + cellState = SelectionStates.EXCLUDED; + } else if (rows[cell.qElemNumber] !== undefined) { + cellState = SelectionStates.SELECTED; + } else { + cellState = SelectionStates.POSSIBLE; + } + } + + return cellState; +}; + +export const handleAnnounceSelectionStatus = (announce: AnnounceFn, rowsLength: number, isAddition: boolean) => { + if (rowsLength) { + const changeStatus = isAddition ? 'SNTable.SelectionLabel.SelectedValue' : 'SNTable.SelectionLabel.DeselectedValue'; + const amountStatus = + rowsLength === 1 + ? 'SNTable.SelectionLabel.OneSelectedValue' + : ['SNTable.SelectionLabel.SelectedValues', rowsLength.toString()]; + announce({ keys: [changeStatus, amountStatus] }); + } else { + announce({ keys: ['SNTable.SelectionLabel.ExitedSelectionMode'] }); + } +}; + +export const getSelectedRows = ( + selectedRows: Record, + cell: TableCell, + evt: React.KeyboardEvent | React.MouseEvent +): Record => { + const { qElemNumber, rowIdx } = cell; + + if (evt.ctrlKey || evt.metaKey) { + // if the ctrl key or the ⌘ Command key (On Macintosh keyboards) + // or the ⊞ Windows key is pressed, get the last clicked + // item (single select) + return { [qElemNumber]: rowIdx }; + } + + const key = (evt as React.KeyboardEvent)?.key; + if (evt.shiftKey && key.includes('Arrow')) { + selectedRows[qElemNumber] = rowIdx; + // add the next or previous cell to selectedRows, based on which arrow is pressed + selectedRows[key === 'ArrowDown' ? cell.nextQElemNumber : cell.prevQElemNumber] = + key === 'ArrowDown' ? rowIdx + 1 : rowIdx - 1; + } else if (selectedRows[qElemNumber] !== undefined) { + // if the selected item is clicked again, that item will be removed + delete selectedRows[qElemNumber]; + } else { + // if an unselected item was clicked, add it to the object + selectedRows[qElemNumber] = rowIdx; + } + + return { ...selectedRows }; +}; + +const selectCell = (state: SelectionState, payload: ActionPayload): SelectionState => { + const { api, rows, colIdx } = state; + const { cell, announce, evt } = payload; + const isSelectMultiValues = evt.shiftKey && (evt as React.KeyboardEvent)?.key.includes('Arrow'); + let selectedRows: Record = {}; + + if (colIdx === -1) api.begin(['/qHyperCubeDef']); + else if (colIdx === cell.colIdx) selectedRows = { ...rows }; + else return state; + + selectedRows = getSelectedRows(selectedRows, cell, evt); + const selectedRowsLength = Object.keys(selectedRows).length; + const isAddition = selectedRowsLength >= Object.keys(rows).length; + handleAnnounceSelectionStatus(announce, selectedRowsLength, isAddition); + + if (selectedRowsLength) { + !isSelectMultiValues && + api.select({ + method: 'selectHyperCubeCells', + params: ['/qHyperCubeDef', Object.values(selectedRows), [cell.colIdx]], + }); + return { ...state, rows: selectedRows, colIdx: cell.colIdx, isSelectMultiValues }; + } + + api.cancel(); + return { ...state, rows: selectedRows, colIdx: -1, isSelectMultiValues }; +}; + +const selectMultiValues = (state: SelectionState): SelectionState => { + const { api, rows, colIdx, isSelectMultiValues } = state; + + isSelectMultiValues && + api.select({ + method: 'selectHyperCubeCells', + params: ['/qHyperCubeDef', Object.values(rows), [colIdx]], + }); + + return { ...state, isSelectMultiValues: false }; +}; + +export const reducer = (state: SelectionState, action: TSelectionActions): SelectionState => { + switch (action.type) { + case SelectionActions.SELECT: + return selectCell(state, action.payload); + case SelectionActions.SELECT_MULTI_VALUES: + return selectMultiValues(state); + case SelectionActions.RESET: + return state.api.isModal() ? state : { ...state, rows: {}, colIdx: -1 }; + case SelectionActions.CLEAR: + return Object.keys(state.rows).length ? { ...state, rows: {} } : state; + default: + throw new Error('reducer called with invalid action type'); + } +}; diff --git a/prebuilt/react-native-sn-table/table/utils/styling-utils.js b/prebuilt/react-native-sn-table/table/utils/styling-utils.js new file mode 100644 index 00000000..53855079 --- /dev/null +++ b/prebuilt/react-native-sn-table/table/utils/styling-utils.js @@ -0,0 +1,253 @@ +import { resolveToRGBAorRGB, isDarkColor, removeOpacity } from './color-utils'; +import { SelectionStates } from './selections-utils'; +// the order of style +// default (inl. sprout theme) < Sense theme < styling settings +// < column < selection (except the selected green) < hover < selected green + +export const STYLING_DEFAULTS = { + FONT_COLOR: '#404040', + HOVER_BACKGROUND: '#f4f4f4', + SELECTED_CLASS: 'selected', + SELECTED_BACKGROUND: '#009845', + EXCLUDED_BACKGROUND: + 'repeating-linear-gradient(-45deg, rgba(200,200,200,0.08), rgba(200,200,200,0.08) 2px, rgba(200,200,200,0.3) 2.5px, rgba(200,200,200,0.08) 3px, rgba(200,200,200,0.08) 5px)', + WHITE: '#fff', + DATA_VIEW_FONTSIZE: 14, +}; + +export const SELECTION_STYLING = { + SELECTED: { + color: STYLING_DEFAULTS.WHITE, + background: STYLING_DEFAULTS.SELECTED_BACKGROUND, + // Setting a specific class for selected cells styling to override hover effect + selectedCellClass: STYLING_DEFAULTS.SELECTED_CLASS, + }, + POSSIBLE: { + color: STYLING_DEFAULTS.FONT_COLOR, + background: STYLING_DEFAULTS.WHITE, + }, +}; + +// Both index !== -1 and color !== null must be true for the property to be set +export const isSet = (prop) => prop && JSON.stringify(prop) !== JSON.stringify({ index: -1, color: null }); + +export function getPadding(styleObj, defaultPadding) { + let padding = defaultPadding; + if (styleObj?.padding) { + ({ padding } = styleObj); + } else if (styleObj?.fontSize) { + padding = `${styleObj.fontSize / 2}px ${styleObj.fontSize}px`; + } + return padding; +} + +export function getColor(defaultColor, theme, color = {}) { + const resolvedColor = theme.getColorPickerColor(color); + // for legacy checks + if (color.color && resolvedColor !== color.color) { + return color.color; + } + return !resolvedColor || resolvedColor === 'none' ? defaultColor : resolvedColor; +} + +export const getAutoFontColor = (backgroundColor) => + isDarkColor(backgroundColor) ? STYLING_DEFAULTS.WHITE : STYLING_DEFAULTS.FONT_COLOR; + +export const getBaseStyling = (styleObj, objetName, theme) => { + const fontFamily = theme.getStyle('object', `straightTable.${objetName}`, 'fontFamily'); + const color = theme.getStyle('object', `straightTable.${objetName}`, 'color'); + const fontSize = theme.getStyle('object', `straightTable.${objetName}`, 'fontSize'); + + const baseStyle = { + fontFamily, + color: isSet(styleObj?.fontColor) ? getColor(STYLING_DEFAULTS.FONT_COLOR, theme, styleObj.fontColor) : color, + fontSize: styleObj?.fontSize ? styleObj.fontSize : fontSize, + padding: getPadding(styleObj, STYLING_DEFAULTS.PADDING), + borderStyle: 'solid', + borderColor: theme?.table?.body.borderColor || 'black', + }; + // Remove all Undefined Values from an Object + Object.keys(baseStyle).forEach((key) => baseStyle[key] == null && delete baseStyle[key]); + return baseStyle; +}; + +export function getHeaderStyle(layout, theme) { + const themeComponent = layout?.components?.find((comp) => comp.key === 'theme'); + const header = themeComponent?.header ? themeComponent.header : layout.components?.[0]?.header; + const headerStyle = getBaseStyling(header, 'header', theme); + headerStyle.cursor = 'pointer'; + headerStyle.borderWidth = '1px 1px 1px 0px'; + if (layout.isDataView) { + headerStyle.fontSize = STYLING_DEFAULTS.DATA_VIEW_FONTSIZE; + headerStyle.rowHeight = 2; + } + + // To avoid seeing the table body through the table head: + // - When the table background color from the sense theme is transparent, + // there is a header background color depending on the header font color + // - When the table background color from the sense theme has opacity, + // removing that. + const headerBackgroundColor = isDarkColor(headerStyle.color) ? '#FAFAFA' : '#323232'; + headerStyle.backgroundColor = theme?.table?.isBackgroundTransparentColor + ? headerBackgroundColor + : removeOpacity(theme?.table?.backgroundColor); + + // When you set the header font color, + // the sort label color should be same. + // When there is no header content color setting, + // the sort label color is depending on the header background color. + headerStyle.sortLabelColor = + headerStyle.color ?? (isDarkColor(headerStyle?.backgroundColor) ? 'rgba(255,255,255,0.9)' : 'rgba(0, 0, 0, 0.54)'); + + headerStyle.wrap = layout?.multiline?.wrapTextInHeaders; + + return headerStyle; +} + +export function getBodyCellStyle(layout, theme) { + const themeComponent = layout?.components?.find((comp) => comp.key === 'theme'); + const content = themeComponent?.content ? themeComponent.content : layout.components?.[0]?.content; + const contentStyle = getBaseStyling(content, 'content', theme); + contentStyle.borderWidth = '0px 1px 1px 0px'; + let rowHeight = content?.rowHeight || 1; + if (layout.isDataView) { + contentStyle.fontSize = STYLING_DEFAULTS.DATA_VIEW_FONTSIZE; + rowHeight = 2; + } + const hoverBackgroundColorFromLayout = content?.hoverColor; + const hoverFontColorFromLayout = content?.hoverFontColor; + + const hoverBackgroundColorFromTheme = theme.getStyle('object', '', 'straightTable.content.hover.backgroundColor'); + const hoverFontColorFromTheme = theme.getStyle('object', '', 'straightTable.content.hover.color'); + + // Cases when hoverEffect is true: + // 1. There is no hover font color but a hover background color, + // when hovering, the hover font color becomes white when the hover + // background color is a dark color or the hover font color stays + // the same as whatever the font color is when the hover background + // color is a light color. + // 2. There is no hover font color and no hover background color, + // when hovering, the default hover effect (a light gray hover background + // color and no hover font color) is in use. + // 3. There is a hover font color but no hover background color, + // when hovering, only the font color is applied and no hover + // background color is shown. + // 4. There are both a hover font and a hover background color, + // when hovering, the hover font and the hover background color take effect. + + // Note: Hover colors from Layout have a higher priority than those from theme. + + const isHoverFontColorSet = isSet(hoverFontColorFromLayout) || !!hoverFontColorFromTheme; + const isHoverBackgroundColorSet = isSet(hoverBackgroundColorFromLayout) || !!hoverBackgroundColorFromTheme; + const isHoverFontOrBackgroundColorSet = isHoverFontColorSet || isHoverBackgroundColorSet; + + let hoverBackgroundColor; + if (isSet(hoverBackgroundColorFromLayout)) { + // case 1 or 4 + hoverBackgroundColor = getColor(STYLING_DEFAULTS.HOVER_BACKGROUND, theme, hoverBackgroundColorFromLayout); + } else if (hoverBackgroundColorFromTheme) { + // case 1 or 4 + hoverBackgroundColor = hoverBackgroundColorFromTheme; + } else if (isHoverFontColorSet) { + hoverBackgroundColor = ''; // case 3 + } else { + hoverBackgroundColor = STYLING_DEFAULTS.HOVER_BACKGROUND; // case 2 + } + + const hoverFontColor = isHoverFontOrBackgroundColorSet + ? getColor( + getAutoFontColor(hoverBackgroundColor), + theme, + isSet(hoverFontColorFromLayout) ? hoverFontColorFromLayout : hoverFontColorFromTheme + ) // case 1 or 3 or 4 + : ''; // case 2; + + return { + ...contentStyle, + hoverBackgroundColor, + hoverFontColor, + rowHeight, + wrap: layout?.multiline?.wrapTextInCells, + }; +} + +export function getTotalsCellStyle(layout, theme) { + const headerStyle = getHeaderStyle(layout, theme); + return { ...getBodyCellStyle(layout, theme), backgroundColor: headerStyle.backgroundColor }; +} + +/** + * You can set the background color expression and/or text color expression + * for measure data and/or dimension data. + * Ex: + * {"qHyperCubeDef": { + * "qDimensions": [{ + * "qAttributeExpressions": [ + { + "qExpression": "rgb(4,4,4)", + "qAttribute": true, + "id": "cellBackgroundColor" + }, + { + "qExpression": "rgb(219, 42, 42)", + "qAttribute": true, + "id": "cellForegroundColor" + } + ], + * }] + * }} + * + * get style from qAttributeExpressions in qDimensions or qMeasures + * @param {Object} styling - style from styling in CellRenderer in TableBodyWrapper + * @param {?Object} qAttrExps - qAttributeExpressions from each cell + * @param {Array} stylingInfo - stylingInfo from each column + * @returns {Object} cell font color and background color used for cells in specific columns + */ +export function getColumnStyle(styling, qAttrExps, stylingInfo) { + const columnColors = {}; + qAttrExps?.qValues.forEach((val, i) => { + columnColors[stylingInfo[i]] = resolveToRGBAorRGB(val.qText); + }); + + if (columnColors.cellBackgroundColor && !columnColors.cellForegroundColor) + columnColors.cellForegroundColor = getAutoFontColor(columnColors.cellBackgroundColor); + + return { + ...styling, + color: columnColors.cellForegroundColor || styling.color, + backgroundColor: columnColors.cellBackgroundColor, + }; +} + +/** + * Get the style for one cell based on wether it is + * selected, possible, excluded or no extra styling at all (not in selection mode) + * @param {Object} styling - Styling already calculated for the cell + * @param {String} cellSelectionState - The selection state the cell is in + * @param {?String} [themeBackgroundColor='#fff'] - The background color from nebula theme or sense theme + * @returns {Object} The style for the cell + */ + +export function getSelectionStyle(styling, cellSelectionState) { + let selectionStyling = {}; + switch (cellSelectionState) { + case SelectionStates.SELECTED: + selectionStyling = SELECTION_STYLING.SELECTED; + break; + case SelectionStates.POSSIBLE: + selectionStyling = SELECTION_STYLING.POSSIBLE; + break; + case SelectionStates.EXCLUDED: + selectionStyling = { + background: `${STYLING_DEFAULTS.EXCLUDED_BACKGROUND}, ${styling.backgroundColor}`, + }; + break; + default: + break; + } + + return { + ...styling, + ...selectionStyling, + }; +} diff --git a/prebuilt/react-native-sn-table/types.ts b/prebuilt/react-native-sn-table/types.ts new file mode 100644 index 00000000..c7ac70e7 --- /dev/null +++ b/prebuilt/react-native-sn-table/types.ts @@ -0,0 +1,51 @@ +import React from 'react'; +import { stardust } from '@nebula.js/stardust'; + +export interface TableCell { + qText: string | undefined; + qAttrExps: EngineAPI.INxAttributeExpressionValues; + qElemNumber: number; + rowIdx: number; + colIdx: number; + isSelectable: boolean; + rawRowIdx: number; + rawColIdx: number; + prevQElemNumber: number; + nextQElemNumber: number; + qNum?: number; +} + +export interface ExtendedSelectionAPI extends stardust.ObjectSelections { + on(eventType: string, callback: () => void): void; + removeListener(eventType: string, callback: () => void): void; +} + +export interface SelectionState { + rows: Record; + colIdx: number; + api: ExtendedSelectionAPI; + isSelectMultiValues: boolean; +} + +export interface AnnounceArgs { + keys: Array>; + shouldBeAtomic?: boolean; + politeness?: 'polite' | 'assertive' | 'off'; +} + +export type AnnounceFn = (arg0: AnnounceArgs) => void; + +export interface ActionPayload { + cell: TableCell; + announce: (arg0: AnnounceArgs) => void; + evt: React.KeyboardEvent | React.MouseEvent; +} + +export interface ContextValue { + headRowHeight: number; + setHeadRowHeight: React.Dispatch>; + focusedCellCoord: [number, number]; + setFocusedCellCoord: React.Dispatch>; + selectionState: SelectionState; + selectionDispatch: React.Dispatch | jest.Mock; +}