diff --git a/packages/core/components/ComboBox/ComboBox.tsx b/packages/core/components/ComboBox/ComboBox.tsx index 1144875928..e79f732bc5 100644 --- a/packages/core/components/ComboBox/ComboBox.tsx +++ b/packages/core/components/ComboBox/ComboBox.tsx @@ -121,7 +121,6 @@ const ComboBox: React.FC = ({ setQuery('') setFocused(false) setActiveIndex(-1) - inputRef.current?.blur() } // Handle input change (typing) @@ -181,7 +180,6 @@ const ComboBox: React.FC = ({ setQuery('') setFocused(false) setActiveIndex(-1) - inputRef.current?.blur() break case 'Tab': diff --git a/packages/core/components/ComboBox/combobox.styles.css b/packages/core/components/ComboBox/combobox.styles.css index eb8ab507c7..dea12a8917 100644 --- a/packages/core/components/ComboBox/combobox.styles.css +++ b/packages/core/components/ComboBox/combobox.styles.css @@ -9,6 +9,11 @@ display: flex; position: relative; width: 100%; + + &:focus-within { + outline: dashed 2px rgb(0, 122, 153) !important; + outline-offset: 3px !important; + } } input.cove-combobox-input[role='combobox'] { @@ -36,12 +41,12 @@ border: 1px solid var(--cool-gray-10) !important; box-shadow: none; color: var(--cool-gray-90); + outline: none !important; } input.cove-combobox-input[role='combobox']:focus-visible { border-radius: 6px !important; - outline: dashed 2px rgb(0, 122, 153) !important; - outline-offset: 3px !important; + outline: none !important; } input.cove-combobox-input[role='combobox']:disabled { @@ -119,9 +124,9 @@ color: var(--cool-gray-90); cursor: pointer; font-family: var(--app-font-secondary); - font-size: 0.778rem; + font-size: 0.883rem; font-weight: 300; - padding: 0.5rem 0.5rem; + padding: 0.3rem 0.3rem; transition: background-color 0.15s ease-in-out; user-select: none; } diff --git a/packages/core/components/ComboBox/tests/ComboBox.test.tsx b/packages/core/components/ComboBox/tests/ComboBox.test.tsx new file mode 100644 index 0000000000..4a9c8c7de0 --- /dev/null +++ b/packages/core/components/ComboBox/tests/ComboBox.test.tsx @@ -0,0 +1,57 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import ComboBox from '../ComboBox' + +vi.mock('../../../assets/icon-magnifying-glass.svg', () => ({ + default: props => +})) + +const options = [ + { value: '2023', label: '2023' }, + { value: '2024', label: '2024' }, + { value: '2025', label: '2025' } +] + +const renderComboBox = (selected = '2023') => { + const updateField = vi.fn() + + render( + + ) + + return { + input: screen.getByRole('combobox'), + updateField + } +} + +describe('ComboBox', () => { + it('keeps focus on the input when escape closes the listbox', () => { + const { input } = renderComboBox() + + input.focus() + fireEvent.keyDown(input, { key: 'ArrowDown' }) + + expect(input).toHaveAttribute('aria-expanded', 'true') + + fireEvent.keyDown(input, { key: 'Escape' }) + + expect(input).toHaveFocus() + expect(input).toHaveAttribute('aria-expanded', 'false') + expect(input).toHaveDisplayValue('2023') + }) + + it('selects the active option with enter and restores the closed display', () => { + const { input, updateField } = renderComboBox() + + input.focus() + fireEvent.change(input, { target: { value: '2024' } }) + fireEvent.keyDown(input, { key: 'ArrowDown' }) + fireEvent.keyDown(input, { key: 'Enter' }) + + expect(updateField).toHaveBeenCalledWith(null, null, 'year', '2024') + expect(input).toHaveFocus() + expect(input).toHaveAttribute('aria-expanded', 'false') + expect(input).toHaveDisplayValue('2023') + }) +}) diff --git a/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.test.tsx b/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.test.tsx new file mode 100644 index 0000000000..5b75d50c6b --- /dev/null +++ b/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.test.tsx @@ -0,0 +1,104 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import NestedDropdownEditor from './NestedDropdownEditor' + +describe('NestedDropdownEditor', () => { + it('renders the subgroup-only checkbox below Create query parameters and defaults it to unchecked', () => { + const updateField = vi.fn() + + render( + + ) + + const queryParameters = screen.getByLabelText('Create query parameters') + const displaySubgroupingOnly = screen.getByLabelText('Display subgrouping only') + + expect(displaySubgroupingOnly).not.toBeChecked() + + const queryParametersLabel = queryParameters.closest('label') + const displaySubgroupingOnlyLabel = displaySubgroupingOnly.closest('label') + const isBelowQueryParameters = !!( + queryParametersLabel && + displaySubgroupingOnlyLabel && + queryParametersLabel.compareDocumentPosition(displaySubgroupingOnlyLabel) & Node.DOCUMENT_POSITION_FOLLOWING + ) + + expect(isBelowQueryParameters).toBe(true) + + fireEvent.click(displaySubgroupingOnly) + + expect(updateField).toHaveBeenCalledWith('filters', 0, 'displaySubgroupingOnly', true) + }) + + it('does not render the subgroup-only checkbox when the filter is not nested-dropdown', () => { + render( + + ) + + expect(screen.queryByLabelText('Display subgrouping only')).not.toBeInTheDocument() + }) +}) diff --git a/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx b/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx index 2727a939ea..15520df627 100644 --- a/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +++ b/packages/core/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx @@ -217,6 +217,20 @@ const NestedDropdownEditor: React.FC = ({ )} + {filter.filterStyle === 'nested-dropdown' && ( + + )} +
{filter.columnName}
updateFilterProp('displaySubgroupingOnly', e.target.checked)} + /> + Display subgrouping only + + )} + {!!parentFilters.length && ( + + )} = ({ const newSubGrouping = { ...subGrouping, defaultValue: value } updateFilterProp('subGrouping', newSubGrouping) }} - label={'Sub Group Default Value'} + label={'Subgroup Default Value'} initial={'Select'} /> )} diff --git a/packages/dashboard/src/types/SharedFilter.ts b/packages/dashboard/src/types/SharedFilter.ts index 8bdf3a03fb..62aa186610 100644 --- a/packages/dashboard/src/types/SharedFilter.ts +++ b/packages/dashboard/src/types/SharedFilter.ts @@ -8,6 +8,7 @@ export type SharedFilter = FilterBase & { filterStyle: FilterStyle queryParameter?: string setByQueryParameter?: string + displaySubgroupingOnly?: boolean active?: string | string[] queuedActive?: string | string[] usedBy?: (string | number)[] // if number used by whole row, else used by specific viz diff --git a/packages/data-bite/src/components/EditorPanel/EditorPanel.tsx b/packages/data-bite/src/components/EditorPanel/EditorPanel.tsx index 421bcd7b05..009ab09799 100644 --- a/packages/data-bite/src/components/EditorPanel/EditorPanel.tsx +++ b/packages/data-bite/src/components/EditorPanel/EditorPanel.tsx @@ -67,6 +67,7 @@ const EditorPanel: React.FC = () => { const trendMappings = config.trendIndicator?.mappings || [] const isNumericModeEligible = NUMERIC_TREND_ELIGIBLE_FUNCTIONS.has(config.dataFunction) const isPassthroughFunction = config.dataFunction === DATA_FUNCTION_PASSTHROUGH + const supportsTrendIndicator = config.biteStyle === 'tp5' const trendColumnValues = useMemo(() => { const trendColumn = config.trendIndicator?.column @@ -305,164 +306,181 @@ const EditorPanel: React.FC = () => { label='Ignore Zeros' updateField={updateField} /> -
-
- - Trend Indicator - - - - - - -

- Choose the column that contains past data for your metric. It will be run through the - same function selected from the Data Function dropdown. -

-
- - ) : null - } - updateField={updateField} - initial='Select' - options={columns} - /> - {trendMode === TREND_MODE_CATEGORICAL && ( + {supportsTrendIndicator && ( + <> +
+
+ + Trend Indicator + + ({ - value: arrowType, - label: TREND_ARROW_TYPE_LABELS[arrowType] - })) - ]} - onChange={e => updateTrendMapping(sourceValue, e.target.value)} - /> -
-
- ) - })} - - )} - {trendMode === TREND_MODE_NUMERIC && ( -
    -
  • -

    - An arrow will be shown if the difference between the current value and the - historical value exceeds this threshold. + Choose the column that contains past data for your metric. It will be run through + the same function selected from the Data Function dropdown.

    - } + ) : null + } + updateField={updateField} + initial='Select' + options={columns} + /> + {trendMode === TREND_MODE_CATEGORICAL && ( + <> + {!isPassthroughFunction && ( + + In categorical mode, arrows appear only when filters resolve to exactly one row. + + )} + {trendColumnValues.map(sourceValue => { + const selectedArrowType = + trendMappings.find(mapping => mapping.sourceValue === sourceValue)?.arrowType || '' + + return ( +
    +
    {sourceValue}
    +
    + setTrendMode(e.target.value)} - /> - {trendMode === TREND_MODE_NUMERIC && !isNumericModeEligible && ( -

    - Numeric mode only supports Sum, Mean (Average), Median, Min, and Max. -

    - )} - {trendMode && !(trendMode === TREND_MODE_NUMERIC && !isNumericModeEligible) && ( - <> + {supportsTrendIndicator && ( + <> +
    +
    +

    Trend Indicator

    ({ - value: arrowType, - label: TREND_ARROW_TYPE_LABELS[arrowType] - })) - ]} - onChange={e => updateTrendMapping(sourceValue, e.target.value)} - /> -
    -
    - ) - })} - - )} - {trendMode === TREND_MODE_NUMERIC && ( - - - - - -

    - An arrow is shown when the current displayed percentage differs from the historical displayed - percentage by more than this many percentage points. -

    -
    - - } - /> + {trendMode === TREND_MODE_NUMERIC && !isNumericModeEligible && ( +

    + Numeric mode only supports Sum, Mean (Average), Median, Min, and Max. +

    )} - - - {trendMode === TREND_MODE_NUMERIC && ( + {trendMode && !(trendMode === TREND_MODE_NUMERIC && !isNumericModeEligible) && ( <> - + + + + +

    + Choose the column that contains past numerator data for your metric. It will be run + through the same function selected from the Data Function dropdown. +

    +
    + + ) : null + } + updateField={updateField} + initial='Select' + options={columns} + /> + {trendMode === TREND_MODE_CATEGORICAL && ( + <> + + In categorical mode, arrows appear only when filters resolve to exactly one row. + + {trendColumnValues.map(sourceValue => { + const selectedArrowType = + trendMappings.find(mapping => mapping.sourceValue === sourceValue)?.arrowType || '' + return ( +
    +
    {sourceValue}
    +
    +