Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .storybook/preview.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { previewTheme } from './coveSbThemes'
import * as React from 'react'
import '@cdc/core/styles/cove-main.scss'

export const parameters = {
Expand Down
1 change: 1 addition & 0 deletions packages/core/assets/user-icons/arrow-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/core/assets/user-icons/arrow-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ describe('MarkupVariablesEditor', () => {
label.textContent?.replace(/\s+/g, ' ').trim()
)

expect(labels).toEqual(['Source', 'Icon Mode', 'Icon', 'Variable Name', 'Tag (auto-generated)', 'Icon Scale'])
expect(within(basicSettingsItem).getByRole('spinbutton', { name: 'Icon Scale' })).toHaveValue(0.8)
expect(labels).toEqual(['Source', 'Icon Mode', 'Icon', 'Variable Name', 'Tag (auto-generated)'])
expect(within(basicSettingsItem).queryByRole('spinbutton', { name: 'Icon Scale' })).not.toBeInTheDocument()

const iconButton = basicSettingsItem.querySelector('button[aria-haspopup="listbox"]') as HTMLButtonElement
expect(iconButton).toBeTruthy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
getMarkupVariableSourceType,
isDataDrivenIconsVariable
} from '../../../types/MarkupVariable'
import { DEFAULT_SVG_SCALE, SVG_REGISTRY_OPTIONS, getSvgRegistryLabel } from '../../../helpers/svgRegistry'
import { SVG_REGISTRY_OPTIONS, getSvgRegistryLabel } from '../../../helpers/svgRegistry'
import Button from '../../elements/Button'
import { TextField, Select, CheckBox } from '../Inputs'
import SvgIconSelect from './SvgIconSelect'
Expand Down Expand Up @@ -39,7 +39,6 @@ export type { MarkupVariablesEditorProps }

const METADATA_DOCS_URL =
'https://www.cdc.gov/cove/data-toolkit/index.html#cdc_toolkit_main_toolkit_cat_3-json-file-format'
const SMALL_NUMBER_FIELD_WRAPPER_STYLE = { maxWidth: '5rem' }
type MarkupVariableEditorSourceType = MarkupVariableSourceType
type MarkupVariableIconMode = 'static' | 'data-driven'

Expand Down Expand Up @@ -181,13 +180,6 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
errors.push('Icon is required')
}

if (variable.svgScale !== undefined) {
const parsedScale = Number(variable.svgScale)
if (!Number.isFinite(parsedScale) || parsedScale <= 0) {
errors.push('Icon scale must be greater than 0')
}
}

return errors
}

Expand Down Expand Up @@ -225,12 +217,6 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
if (!variable.svgMappings || variable.svgMappings.length === 0) {
errors.push('At least one icon mapping is required')
}
if (variable.svgScale !== undefined) {
const parsedScale = Number(variable.svgScale)
if (!Number.isFinite(parsedScale) || parsedScale <= 0) {
errors.push('Icon scale must be greater than 0')
}
}
}

variable.conditions?.forEach((condition, index) => {
Expand Down Expand Up @@ -429,9 +415,6 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
</div>
)

const getNumericFieldValue = (value?: number) =>
value === undefined || Number.isNaN(Number(value)) ? DEFAULT_SVG_SCALE : Number(value)

return (
<div className='markup-variables-editor'>
<div className='mb-3'>
Expand Down Expand Up @@ -682,25 +665,6 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
/>
</div>
{renderReadonlyField('Tag (auto-generated)', variable.tag, '{{icon-name}}')}

<div className='mb-3' style={SMALL_NUMBER_FIELD_WRAPPER_STYLE}>
<TextField
type='number'
step={0.1}
value={getNumericFieldValue(variable.svgScale)}
fieldName='svgScale'
label='Icon Scale'
updateField={(_section, _subsection, _fieldName, value) => {
const parsedScale = value === '' ? undefined : Number(value)
updateVariable(index, {
svgScale:
parsedScale === undefined || Number.isNaN(parsedScale)
? undefined
: parsedScale
})
}}
/>
</div>
</>
)}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/components/MultiSelect/multiselect.styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
background: var(--lightestGray);
}
border: 1px solid var(--cool-gray-10);
border-radius: 5px;
border-radius: 0.333rem;
display: inline-block;
font-family: var(--app-font-secondary);
font-size: var(--filter-select-font-size);
Expand All @@ -26,7 +26,7 @@
}
:is(div) {
background: var(--lightGray);
border-radius: 5px;
border-radius: 0.333rem;
display: inline-block;
margin-bottom: 2px;
margin-right: 5px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
}

.nested-dropdown-input-container {
border-radius: 0.25rem;
border-radius: 0.333rem;
display: block;
padding: calc(0.4rem + 1px) 0.75rem;
position: relative;
Expand Down
51 changes: 51 additions & 0 deletions packages/core/components/ui/TrendArrow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import TrendArrow from './TrendArrow'
import { TREND_ARROW_DOWN, TREND_ARROW_NO_CHANGE, TREND_ARROW_UP } from '../../helpers/trendIndicator'

describe('TrendArrow', () => {
it('renders the up arrow asset with the registry aria label when no visible text is present', () => {
const { container } = render(<TrendArrow arrowType={TREND_ARROW_UP} />)

const icon = screen.getByRole('img', { name: 'Trend up' })
expect(icon).toHaveClass('cove-trend-arrow')
expect(icon).toHaveAttribute('focusable', 'false')
expect(icon).not.toHaveClass('is-down')
expect(icon).not.toHaveClass('is-no-change')
expect(icon).toHaveAttribute('viewBox', '0 0 384 512')
expect(container.querySelector('path')).toBeInTheDocument()
})

it('renders the down arrow asset without brittle path assertions', () => {
const { container } = render(<TrendArrow arrowType={TREND_ARROW_DOWN} />)

const icon = screen.getByRole('img', { name: 'Trend down' })
expect(icon).toHaveClass('cove-trend-arrow')
expect(icon).toHaveAttribute('focusable', 'false')
expect(icon).not.toHaveClass('is-down')
expect(icon).not.toHaveClass('is-no-change')
expect(icon).toHaveAttribute('viewBox', '0 0 384 512')
expect(container.querySelector('path')).toBeInTheDocument()
})

it('renders the no-change asset with visible label text and a decorative icon', () => {
const { container } = render(
<TrendArrow arrowType={TREND_ARROW_NO_CHANGE} wrapperClassName='custom-wrap' label='Steady' />
)

expect(screen.getByText('Steady')).toHaveClass('cove-trend-arrow__label')
const wrapper = container.querySelector('.cove-trend-arrow__wrap.custom-wrap')
expect(wrapper).toBeInTheDocument()

const icon = container.querySelector('svg')
expect(screen.queryByRole('img', { name: 'Trend no change' })).not.toBeInTheDocument()
expect(icon).toHaveAttribute('aria-hidden', 'true')
expect(icon).toHaveAttribute('focusable', 'false')
expect(icon).toHaveClass('cove-trend-arrow')
expect(icon).not.toHaveClass('is-down')
expect(icon).not.toHaveClass('is-no-change')
expect(icon).toHaveAttribute('viewBox', '0 0 512 384')
expect(container.querySelector('path')).toBeInTheDocument()
})
})
31 changes: 18 additions & 13 deletions packages/core/components/ui/TrendArrow.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import parse from 'html-react-parser'
import { TrendArrowType, TREND_ARROW_DOWN, TREND_ARROW_NO_CHANGE } from '../../helpers/trendIndicator'
import TrendArrowIcon from '../../assets/user-icons/arrow-up.svg'
import { SVG_REGISTRY, SvgRegistryId, getSvgRegistryLabel } from '../../helpers/svgRegistry'
import './trend-arrow.css'

type TrendArrowProps = {
arrowType: TrendArrowType
wrapperClassName?: string
ariaLabel?: string
label?: string
}

const TrendArrow = ({ arrowType, wrapperClassName = '', ariaLabel, label }: TrendArrowProps) => {
const isDownArrow = arrowType === TREND_ARROW_DOWN
const isNoChangeArrow = arrowType === TREND_ARROW_NO_CHANGE
const TrendArrow = ({ arrowType, wrapperClassName = '', label }: TrendArrowProps) => {
const svgId: SvgRegistryId =
arrowType === TREND_ARROW_DOWN
? 'trend-arrow-down'
: arrowType === TREND_ARROW_NO_CHANGE
? 'trend-arrow-no-change'
: 'trend-arrow-up'
const rawSvg = SVG_REGISTRY[svgId].rawSvg
const trendArrowWrapClasses = ['cove-trend-arrow__wrap', wrapperClassName].filter(Boolean).join(' ')
const trimmedLabel = label?.trim()
const resolvedAriaLabel = ariaLabel || `Trend ${arrowType}${trimmedLabel ? `: ${trimmedLabel}` : ''}`
const defaultAriaLabel = getSvgRegistryLabel(svgId) || `Trend ${arrowType}`
const iconAccessibilityAttributes = trimmedLabel
? 'aria-hidden="true" focusable="false"'
: `role="img" aria-label="${defaultAriaLabel}" focusable="false"`
const iconMarkup = rawSvg
.trim()
.replace('<svg', `<svg class="cove-trend-arrow" ${iconAccessibilityAttributes}`)

return (
<span className={trendArrowWrapClasses}>
{trimmedLabel && <span className='cove-trend-arrow__label'>{trimmedLabel}</span>}
<TrendArrowIcon
className={['cove-trend-arrow', isDownArrow ? 'is-down' : '', isNoChangeArrow ? 'is-no-change' : '']
.filter(Boolean)
.join(' ')}
role='img'
aria-label={resolvedAriaLabel}
/>
{parse(iconMarkup)}
</span>
)
}
Expand Down
8 changes: 0 additions & 8 deletions packages/core/components/ui/trend-arrow.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,3 @@
.cove-trend-arrow__wrap--inline .cove-trend-arrow__label {
margin-left: 0.25em;
}

.cove-trend-arrow.is-down {
transform: scale(-1, 1) rotate(180deg);
}

.cove-trend-arrow.is-no-change {
transform: rotate(90deg);
}
31 changes: 28 additions & 3 deletions packages/core/generateViteConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,46 @@ const examplesApiPlugin = () => ({
}
})

function isTraversableDirectory(entry, fullPath) {
if (entry.isDirectory()) return true
if (!entry.isSymbolicLink()) return false

try {
return fs.statSync(fullPath).isDirectory()
} catch {
return false
}
}

// Recursively list JSON files in a directory
function listJsonFiles(dir, baseDir) {
function listJsonFiles(dir, baseDir, ancestorRealPaths = new Set()) {
const files = []
if (!fs.existsSync(dir)) return files

let realDir
try {
realDir = fs.realpathSync(dir)
} catch {
return files
}

// Prevent cycles when a symlink points back to an ancestor directory.
if (ancestorRealPaths.has(realDir)) return files

const nextAncestorRealPaths = new Set(ancestorRealPaths)
nextAncestorRealPaths.add(realDir)

const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...listJsonFiles(fullPath, baseDir))
if (isTraversableDirectory(entry, fullPath)) {
files.push(...listJsonFiles(fullPath, baseDir, nextAncestorRealPaths))
} else if (entry.name.endsWith('.json')) {
// Get relative path from examples dir
files.push(path.relative(baseDir, fullPath))
}
}

return files.sort()
}

Expand Down
23 changes: 2 additions & 21 deletions packages/core/helpers/markupProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,7 @@ export const processMarkupVariables = (
const usesDataDrivenIcons = isDataDrivenIconsVariable(workingVariable)

if (sourceType === 'icon') {
const svgMarkup =
workingVariable.iconId && SVG_REGISTRY[workingVariable.iconId]
? buildInlineSvg(workingVariable.iconId, { scale: workingVariable.svgScale })
: ''
const svgMarkup = workingVariable.iconId && SVG_REGISTRY[workingVariable.iconId] ? buildInlineSvg(workingVariable.iconId) : ''

if (showNoDataMessage && svgMarkup === '') {
noDataMessageChecker.push(true)
Expand Down Expand Up @@ -142,9 +139,7 @@ export const processMarkupVariables = (
workingVariable.columnName,
workingVariable.svgMappings || []
)
svgMarkup = resolvedSvgIds.length
? resolvedSvgIds.map(svgId => buildInlineSvg(svgId, { scale: workingVariable.svgScale })).join(' ')
: ''
svgMarkup = resolvedSvgIds.length ? resolvedSvgIds.map(svgId => buildInlineSvg(svgId)).join(' ') : ''
}

if (showNoDataMessage && svgMarkup === '') {
Expand Down Expand Up @@ -304,13 +299,6 @@ export const validateMarkupVariables = (markupVariables: MarkupVariable[], data:
errors.push(`Variable ${index + 1}: Icon is required`)
}

if (variable.svgScale !== undefined) {
const parsedScale = Number(variable.svgScale)
if (!Number.isFinite(parsedScale) || parsedScale <= 0) {
errors.push(`Variable ${index + 1}: Icon scale must be greater than 0`)
}
}

return
}

Expand Down Expand Up @@ -341,13 +329,6 @@ export const validateMarkupVariables = (markupVariables: MarkupVariable[], data:
if (!variable.svgMappings || variable.svgMappings.length === 0) {
errors.push(`Variable ${index + 1}: Icon mappings are required`)
}

if (variable.svgScale !== undefined) {
const parsedScale = Number(variable.svgScale)
if (!Number.isFinite(parsedScale) || parsedScale <= 0) {
errors.push(`Variable ${index + 1}: Icon scale must be greater than 0`)
}
}
}

variable.conditions.forEach((condition, condIndex) => {
Expand Down
Loading
Loading