@gooddata/sdk-ui-filters 11.40.0-alpha.5 → 11.40.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/esm/MeasureValueFilter/ComparisonInput.d.ts +7 -1
  2. package/esm/MeasureValueFilter/ComparisonInput.js +22 -12
  3. package/esm/MeasureValueFilter/ConditionInputSection.d.ts +5 -0
  4. package/esm/MeasureValueFilter/ConditionInputSection.js +3 -3
  5. package/esm/MeasureValueFilter/Dropdown.d.ts +3 -0
  6. package/esm/MeasureValueFilter/Dropdown.js +2 -2
  7. package/esm/MeasureValueFilter/DropdownBody.d.ts +4 -0
  8. package/esm/MeasureValueFilter/DropdownBody.js +10 -6
  9. package/esm/MeasureValueFilter/MeasureValueFilter.js +6 -4
  10. package/esm/MeasureValueFilter/MeasureValueFilterButton.d.ts +7 -1
  11. package/esm/MeasureValueFilter/MeasureValueFilterButton.js +2 -2
  12. package/esm/MeasureValueFilter/MeasureValueFilterDropdown.d.ts +7 -0
  13. package/esm/MeasureValueFilter/MeasureValueFilterDropdown.js +2 -2
  14. package/esm/MeasureValueFilter/OperatorDropdown.d.ts +1 -0
  15. package/esm/MeasureValueFilter/OperatorDropdown.js +18 -8
  16. package/esm/MeasureValueFilter/OperatorDropdownBody.d.ts +5 -0
  17. package/esm/MeasureValueFilter/OperatorDropdownBody.js +79 -14
  18. package/esm/MeasureValueFilter/PreviewSection.d.ts +5 -0
  19. package/esm/MeasureValueFilter/PreviewSection.js +33 -4
  20. package/esm/MeasureValueFilter/RangeInput.d.ts +7 -1
  21. package/esm/MeasureValueFilter/RangeInput.js +17 -8
  22. package/esm/MeasureValueFilter/TreatNullValuesAsZeroCheckbox.d.ts +2 -1
  23. package/esm/MeasureValueFilter/TreatNullValuesAsZeroCheckbox.js +8 -5
  24. package/esm/MeasureValueFilter/WarningMessage.js +1 -1
  25. package/esm/MeasureValueFilter/typings.d.ts +16 -0
  26. package/esm/locales.js +15 -0
  27. package/esm/sdk-ui-filters.d.ts +29 -0
  28. package/package.json +16 -16
  29. package/styles/css/attributeFilter.css +11 -1
  30. package/styles/css/attributeFilter.css.map +1 -1
  31. package/styles/css/attributeFilterNext/attributeFilterDropdownButton.css +11 -1
  32. package/styles/css/attributeFilterNext/attributeFilterDropdownButton.css.map +1 -1
  33. package/styles/css/attributeFilterNext.css +11 -1
  34. package/styles/css/attributeFilterNext.css.map +1 -1
  35. package/styles/css/main.css +16 -1
  36. package/styles/css/main.css.map +1 -1
  37. package/styles/css/measureValueFilter.css +5 -0
  38. package/styles/css/measureValueFilter.css.map +1 -1
  39. package/styles/scss/measureValueFilter.scss +7 -2
  40. package/esm/MeasureValueFilter/OperatorDropdownItem.d.ts +0 -12
  41. package/esm/MeasureValueFilter/OperatorDropdownItem.js +0 -39
@@ -10,6 +10,12 @@ interface IComparisonInputProps {
10
10
  hasError?: boolean;
11
11
  ariaDescribedBy?: string;
12
12
  separators?: ISeparators;
13
+ /**
14
+ * 1-based condition position. When set, it is appended to the accessible label so a filter
15
+ * with multiple conditions does not expose several identically-named inputs (WCAG 2.4.6) —
16
+ * a unique id alone is not enough, the checker compares the resolved name text.
17
+ */
18
+ conditionNumber?: number;
13
19
  }
14
- export declare function ComparisonInput({ value, usePercentage, disableAutofocus, onValueChange, onEnterKeyPress, onBlur, hasError, ariaDescribedBy, separators }: IComparisonInputProps): ReactElement;
20
+ export declare function ComparisonInput({ value, usePercentage, disableAutofocus, onValueChange, onEnterKeyPress, onBlur, hasError, ariaDescribedBy, separators, conditionNumber }: IComparisonInputProps): ReactElement;
15
21
  export {};
@@ -1,15 +1,25 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useIntl } from "react-intl";
3
- import { InputWithNumberFormat } from "@gooddata/sdk-ui-kit";
4
- export function ComparisonInput({ value, usePercentage, disableAutofocus, onValueChange, onEnterKeyPress, onBlur, hasError, ariaDescribedBy, separators, }) {
3
+ import { InputWithNumberFormat, useIdPrefixed } from "@gooddata/sdk-ui-kit";
4
+ export function ComparisonInput({ value, usePercentage, disableAutofocus, onValueChange, onEnterKeyPress, onBlur, hasError, ariaDescribedBy, separators, conditionNumber, }) {
5
5
  const intl = useIntl();
6
- return (_jsx(InputWithNumberFormat, { className: "s-mvf-comparison-value-input", dataTestId: "mvf-comparison-value-input", value: value ?? undefined, onEnterKeyPress: onEnterKeyPress, onChange: (val) => onValueChange(val), onBlur: onBlur ? () => onBlur() : undefined, hasError: hasError, isSmall: true, autofocus: !disableAutofocus, suffix: usePercentage ? "%" : "", accessibilityConfig: {
7
- suffixAriaLabel: usePercentage
8
- ? intl.formatMessage({
9
- id: "input.unit.percent",
10
- })
11
- : undefined,
12
- ariaDescribedBy,
13
- ariaInvalid: hasError ? true : undefined,
14
- }, separators: separators }));
6
+ // Label is provided via a hidden element referenced by aria-labelledby (rather than a direct
7
+ // aria-label string), following the codebase convention for input labelling.
8
+ const labelId = useIdPrefixed("mvf-comparison-value-label");
9
+ const baseLabel = intl.formatMessage({ id: "mvf.comparisonInput.ariaLabel" });
10
+ const labelText = conditionNumber === undefined
11
+ ? baseLabel
12
+ : intl.formatMessage({ id: "mvf.input.ariaLabel.withCondition" }, { label: baseLabel, number: conditionNumber });
13
+ return (_jsxs(_Fragment, { children: [
14
+ _jsx(InputWithNumberFormat, { className: "s-mvf-comparison-value-input", dataTestId: "mvf-comparison-value-input", autocomplete: "off", value: value ?? undefined, onEnterKeyPress: onEnterKeyPress, onChange: (val) => onValueChange(val), onBlur: onBlur ? () => onBlur() : undefined, hasError: hasError, isSmall: true, autofocus: !disableAutofocus, suffix: usePercentage ? "%" : "", accessibilityConfig: {
15
+ ariaLabelledBy: labelId,
16
+ suffixAriaLabel: usePercentage
17
+ ? intl.formatMessage({
18
+ id: "input.unit.percent",
19
+ })
20
+ : undefined,
21
+ ariaDescribedBy,
22
+ ariaInvalid: hasError ? true : undefined,
23
+ }, separators: separators }), _jsx("span", { className: "sr-only", id: labelId, children: labelText })
24
+ ] }));
15
25
  }
@@ -2,6 +2,11 @@ import { type ISeparators } from "@gooddata/sdk-model";
2
2
  import { type IMeasureValueFilterValue, type MeasureValueFilterOperator } from "./types.js";
3
3
  export interface IConditionInputSectionProps {
4
4
  index: number;
5
+ /**
6
+ * 1-based condition position, passed to the value inputs to disambiguate their accessible
7
+ * names when a filter has multiple conditions (WCAG 2.4.6). Undefined for a single condition.
8
+ */
9
+ conditionNumber?: number;
5
10
  condition: {
6
11
  operator: MeasureValueFilterOperator;
7
12
  value: IMeasureValueFilterValue;
@@ -7,7 +7,7 @@ import { ComparisonInput } from "./ComparisonInput.js";
7
7
  import { RangeInput } from "./RangeInput.js";
8
8
  export const ConditionInputSection = memo(function ConditionInputSection(props) {
9
9
  const intl = useIntl();
10
- const { index, condition, usePercentage, baseDisableAutofocus, separators, onValueChange, onFromChange, onToChange, onValueBlur, onFromBlur, onToBlur, onApply, } = props;
10
+ const { index, conditionNumber, condition, usePercentage, baseDisableAutofocus, separators, onValueChange, onFromChange, onToChange, onValueBlur, onFromBlur, onToBlur, onApply, } = props;
11
11
  if (!condition || condition.operator === "ALL") {
12
12
  return null;
13
13
  }
@@ -25,7 +25,7 @@ export const ConditionInputSection = memo(function ConditionInputSection(props)
25
25
  })
26
26
  : undefined;
27
27
  return (_jsxs(_Fragment, { children: [
28
- _jsx(ComparisonInput, { value: condition.value.value, usePercentage: usePercentage, onValueChange: (v) => onValueChange(index, v), onEnterKeyPress: onApply, onBlur: () => onValueBlur(index), hasError: shouldShowError, ariaDescribedBy: shouldShowError ? errorId : undefined, disableAutofocus: disableAutofocus, separators: separators }), shouldShowError ? (_jsx("div", { id: errorId, className: "gd-mvf-input-error s-mvf-input-error", "data-testid": errorId, role: "alert", children: validationErrorText })) : null] }));
28
+ _jsx(ComparisonInput, { value: condition.value.value, usePercentage: usePercentage, onValueChange: (v) => onValueChange(index, v), onEnterKeyPress: onApply, onBlur: () => onValueBlur(index), hasError: shouldShowError, ariaDescribedBy: shouldShowError ? errorId : undefined, disableAutofocus: disableAutofocus, separators: separators, conditionNumber: conditionNumber }), shouldShowError ? (_jsx("div", { id: errorId, className: "gd-mvf-input-error s-mvf-input-error", "data-testid": errorId, role: "alert", children: validationErrorText })) : null] }));
29
29
  }
30
30
  if (isRangeConditionOperator(condition.operator)) {
31
31
  const { from = null, to = null } = condition.value;
@@ -43,7 +43,7 @@ export const ConditionInputSection = memo(function ConditionInputSection(props)
43
43
  errorText: fromErrorText,
44
44
  }, toField: {
45
45
  errorText: toErrorText,
46
- }, disableAutofocus: disableAutofocus, separators: separators }));
46
+ }, disableAutofocus: disableAutofocus, separators: separators, conditionNumber: conditionNumber }));
47
47
  }
48
48
  return null;
49
49
  });
@@ -29,6 +29,7 @@ interface IDropdownProps extends IMeasureValueFilterCustomComponentProps {
29
29
  insightDimensionality?: IDimensionalityItem[];
30
30
  isDimensionalityEnabled?: boolean;
31
31
  isFilterSummaryEnabled?: boolean;
32
+ showSimplifiedSummary?: boolean;
32
33
  catalogDimensionality?: IDimensionalityItem[];
33
34
  loadCatalogDimensionality?: (dimensionality: ObjRefInScope[]) => Promise<IDimensionalityItem[]>;
34
35
  onDimensionalityChange?: (dimensionality: ObjRefInScope[]) => void;
@@ -38,6 +39,8 @@ interface IDropdownProps extends IMeasureValueFilterCustomComponentProps {
38
39
  alignPoints?: IAlignPoint[];
39
40
  fullscreenOnMobile?: boolean;
40
41
  mobileHeader?: ReactNode;
42
+ dialogId?: string;
43
+ isViewMode?: boolean;
41
44
  }
42
45
  export declare function Dropdown(props: IDropdownProps): import("react/jsx-runtime").JSX.Element;
43
46
  export {};
@@ -13,14 +13,14 @@ const alignPoints = ["bl tl", "tl bl", "br tr", "tr br"];
13
13
  const DROPDOWN_ALIGNMENTS = alignPoints.map((align) => ({ align, offset: { x: 1, y: 0 } }));
14
14
  const MOBILE_DROPDOWN_ALIGN_POINTS = [{ align: "tl tl" }];
15
15
  const DropdownWithIntl = memo(function DropdownWithIntl(props) {
16
- const { operator = "ALL", usePercentage, warningMessage, locale, onCancel, anchorEl, separators, format, useShortFormat, measureTitle, displayTreatNullAsZeroOption = false, treatNullAsZeroValue = false, enableOperatorSelection, onApply: onApplyProp, onChange: onChangeProp, withoutApply, BodyComponent, DropdownActionsComponent, dimensionality, insightDimensionality, isDimensionalityEnabled, isFilterSummaryEnabled = true, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, conditions = [], enableMultipleConditions = false, enableRankingWithMvf, applyOnResult, loadMetricDetails, isHeaderEnabled, alignPoints, fullscreenOnMobile, mobileHeader, } = props;
16
+ const { operator = "ALL", usePercentage, warningMessage, locale, onCancel, anchorEl, separators, format, useShortFormat, measureTitle, displayTreatNullAsZeroOption = false, treatNullAsZeroValue = false, enableOperatorSelection, onApply: onApplyProp, onChange: onChangeProp, withoutApply, BodyComponent, DropdownActionsComponent, dimensionality, insightDimensionality, isDimensionalityEnabled, isFilterSummaryEnabled = true, showSimplifiedSummary = false, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, conditions = [], enableMultipleConditions = false, enableRankingWithMvf, applyOnResult, loadMetricDetails, isHeaderEnabled, alignPoints, fullscreenOnMobile, mobileHeader, dialogId, isViewMode, } = props;
17
17
  const isMobile = useMediaQuery("mobileDevice");
18
18
  const useFullScreen = !!fullscreenOnMobile && isMobile;
19
19
  const onApply = useCallback((conditions, newDimensionality, applyOnResult) => {
20
20
  onApplyProp(conditions, newDimensionality, applyOnResult);
21
21
  }, [onApplyProp]);
22
22
  const selectedOperator = operator === null ? "ALL" : operator;
23
- const body = (_jsx(UiFocusManager, { enableFocusTrap: true, enableAutofocus: true, enableReturnFocusOnUnmount: true, children: _jsx(DropdownBody, { operator: selectedOperator, conditions: conditions, enableMultipleConditions: enableMultipleConditions, usePercentage: usePercentage, warningMessage: warningMessage, locale: locale, onChange: onChangeProp, withoutApply: withoutApply, BodyComponent: BodyComponent, DropdownActionsComponent: DropdownActionsComponent, onCancel: onCancel, onApply: onApply, separators: separators, format: format, useShortFormat: useShortFormat, measureTitle: measureTitle, displayTreatNullAsZeroOption: displayTreatNullAsZeroOption, treatNullAsZeroValue: treatNullAsZeroValue, enableOperatorSelection: enableOperatorSelection, dimensionality: dimensionality, insightDimensionality: insightDimensionality, isDimensionalityEnabled: isDimensionalityEnabled, isFilterSummaryEnabled: isFilterSummaryEnabled, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, onDimensionalityChange: onDimensionalityChange, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, enableRankingWithMvf: enableRankingWithMvf, applyOnResult: applyOnResult, loadMetricDetails: loadMetricDetails, isHeaderEnabled: isHeaderEnabled, isMobile: useFullScreen }) }));
23
+ const body = (_jsx(UiFocusManager, { enableFocusTrap: true, enableAutofocus: true, enableReturnFocusOnUnmount: true, children: _jsx(DropdownBody, { operator: selectedOperator, conditions: conditions, enableMultipleConditions: enableMultipleConditions, usePercentage: usePercentage, warningMessage: warningMessage, locale: locale, onChange: onChangeProp, withoutApply: withoutApply, BodyComponent: BodyComponent, DropdownActionsComponent: DropdownActionsComponent, onCancel: onCancel, onApply: onApply, separators: separators, format: format, useShortFormat: useShortFormat, measureTitle: measureTitle, displayTreatNullAsZeroOption: displayTreatNullAsZeroOption, treatNullAsZeroValue: treatNullAsZeroValue, enableOperatorSelection: enableOperatorSelection, dimensionality: dimensionality, insightDimensionality: insightDimensionality, isDimensionalityEnabled: isDimensionalityEnabled, isFilterSummaryEnabled: isFilterSummaryEnabled, showSimplifiedSummary: showSimplifiedSummary, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, onDimensionalityChange: onDimensionalityChange, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, enableRankingWithMvf: enableRankingWithMvf, applyOnResult: applyOnResult, loadMetricDetails: loadMetricDetails, isHeaderEnabled: isHeaderEnabled, isMobile: useFullScreen, isViewMode: isViewMode, dialogId: dialogId }) }));
24
24
  if (useFullScreen) {
25
25
  return (_jsx(FullScreenOverlay, { alignTo: "body", alignPoints: MOBILE_DROPDOWN_ALIGN_POINTS, onClose: onCancel, children: _jsxs("div", { className: "gd-mobile-dropdown-overlay overlay gd-flex-row-container gd-mvf-mobile-dropdown", children: [mobileHeader ? (_jsx("div", { className: "gd-mobile-dropdown-header gd-flex-item gd-mvf-mobile-dropdown-header gd-is-mobile", children: mobileHeader })) : null, _jsx("div", { className: "gd-mobile-dropdown-content gd-flex-item-stretch gd-flex-row-container gd-mvf-mobile-dropdown-content", children: body })
26
26
  ] }) }));
@@ -27,6 +27,7 @@ interface IDropdownBodyProps extends IMeasureValueFilterCustomComponentProps {
27
27
  insightDimensionality?: IDimensionalityItem[];
28
28
  isDimensionalityEnabled?: boolean;
29
29
  isFilterSummaryEnabled?: boolean;
30
+ showSimplifiedSummary?: boolean;
30
31
  catalogDimensionality?: IDimensionalityItem[];
31
32
  loadCatalogDimensionality?: (dimensionality: ObjRefInScope[]) => Promise<IDimensionalityItem[]>;
32
33
  onDimensionalityChange?: (dimensionality: ObjRefInScope[]) => void;
@@ -34,6 +35,9 @@ interface IDropdownBodyProps extends IMeasureValueFilterCustomComponentProps {
34
35
  loadMetricDetails?: () => Promise<IMeasureMetadataObject | undefined>;
35
36
  isHeaderEnabled?: boolean;
36
37
  isMobile?: boolean;
38
+ isViewMode?: boolean;
39
+ /** Stable id of the dialog, referenced by the trigger button's aria-controls. */
40
+ dialogId?: string;
37
41
  }
38
42
  export declare const DropdownBodyWithIntl: import("react").NamedExoticComponent<IDropdownBodyProps>;
39
43
  export declare const DropdownBody: import("react").NamedExoticComponent<IDropdownBodyProps>;
@@ -73,7 +73,7 @@ export const DropdownBodyWithIntl = memo(function DropdownBodyWithIntl(props) {
73
73
  const conditionsJoinerOr = intl.formatMessage({
74
74
  id: "mvf.conditionsJoiner.or",
75
75
  });
76
- const { operator: propsOperator, conditions: propsConditions, enableMultipleConditions = false, enableRankingWithMvf = false, applyOnResult: initialApplyOnResult, usePercentage, treatNullAsZeroValue, valuePrecision = DefaultValuePrecision, isDimensionalityEnabled = true, isFilterSummaryEnabled = true, insightDimensionality, separators, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, } = props;
76
+ const { operator: propsOperator, conditions: propsConditions, enableMultipleConditions = false, enableRankingWithMvf = false, applyOnResult: initialApplyOnResult, usePercentage, treatNullAsZeroValue, valuePrecision = DefaultValuePrecision, isDimensionalityEnabled = true, isFilterSummaryEnabled = true, showSimplifiedSummary = false, insightDimensionality, separators, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, } = props;
77
77
  // This flag determines if the message, which explains different filter behavior, is shown for the filters
78
78
  // created before the dimensionality feature was introduced. The new filters or re-saved filters are
79
79
  // considered as "migrated" and the message is not shown.
@@ -552,24 +552,28 @@ export const DropdownBodyWithIntl = memo(function DropdownBodyWithIntl(props) {
552
552
  isCommitPending.current = true;
553
553
  setApplyOnResult(event.target.checked);
554
554
  }, [setApplyOnResult]);
555
- return (_jsxs("div", { className: cx(MEASURE_VALUE_FILTER_DROPDOWN_BODY_CLASS, "gd-dialog gd-dropdown overlay s-mvf-dropdown-body", { "gd-is-mobile": props.isMobile }), "data-testid": "mvf-dropdown-body", children: [props.isHeaderEnabled && props.measureTitle && !props.isMobile ? (_jsx(MeasureValueFilterDropdownHeader, { title: props.measureTitle, loadMetricDetails: props.loadMetricDetails })) : null, _jsx("div", { className: "gd-mvf-dropdown-content", children: BodyComponent ? (_jsx(BodyComponent, { onApplyButtonClick: onApply, onCancelButtonClick: onCancel ?? (() => undefined) })) : (_jsxs(_Fragment, { children: [warningMessage ? (_jsx("div", { className: "gd-mvf-dropdown-section", children: _jsx(WarningMessageComponent, { warningMessage: warningMessage }) })) : null, _jsxs("div", { className: "gd-mvf-conditions-scroll-container", children: [(enableMultipleConditions ? state.conditions : state.conditions.slice(0, 1)).map((c, idx) => (_jsxs("div", { className: cx("gd-mvf-dropdown-section", "gd-mvf-condition-section", {
555
+ return (_jsxs("div", { id: props.dialogId, role: "dialog", "aria-modal": true, "aria-label": props.measureTitle, className: cx(MEASURE_VALUE_FILTER_DROPDOWN_BODY_CLASS, "gd-dialog gd-dropdown overlay s-mvf-dropdown-body", { "gd-is-mobile": props.isMobile }), "data-testid": "mvf-dropdown-body", children: [props.isHeaderEnabled && props.measureTitle && !props.isMobile ? (_jsx(MeasureValueFilterDropdownHeader, { title: props.measureTitle, loadMetricDetails: props.loadMetricDetails })) : null, _jsx("div", { className: "gd-mvf-dropdown-content", children: BodyComponent ? (_jsx(BodyComponent, { onApplyButtonClick: onApply, onCancelButtonClick: onCancel ?? (() => undefined) })) : (_jsxs(_Fragment, { children: [warningMessage ? (_jsx("div", { className: "gd-mvf-dropdown-section", children: _jsx(WarningMessageComponent, { warningMessage: warningMessage }) })) : null, _jsxs("div", { className: "gd-mvf-conditions-scroll-container", children: [(enableMultipleConditions ? state.conditions : state.conditions.slice(0, 1)).map((c, idx) => (_jsxs("div", { className: cx("gd-mvf-dropdown-section", "gd-mvf-condition-section", {
556
556
  "gd-mvf-condition-section--multi": enableMultipleConditions,
557
557
  }), children: [
558
558
  _jsxs("div", { className: "gd-mvf-condition-header", "data-testid": `mvf-condition-${idx}`, children: [
559
- _jsx("div", { className: "gd-mvf-condition-operator", children: _jsx(OperatorDropdown, { onSelect: (op) => handleOperatorSelection(idx, op), operator: c.operator, isDisabled: !enableOperatorSelection, isAllOperatorDisabled: isAllOperatorDisabled, isMobile: props.isMobile }) }), enableMultipleConditions ? (_jsx("div", { className: "gd-mvf-condition-action", children: idx === 0 ? (_jsx(ConditionActionButton, { icon: "plus", isMobile: props.isMobile, isDisabled: isAddConditionDisabled, onClick: handleAddCondition, dataTestId: "mvf-add-condition", label: addConditionTooltip, tooltip: isAddConditionDisabled
559
+ _jsx("div", { className: "gd-mvf-condition-operator", children: _jsx(OperatorDropdown, { onSelect: (op) => handleOperatorSelection(idx, op), operator: c.operator, isDisabled: !enableOperatorSelection, isAllOperatorDisabled: isAllOperatorDisabled, isMobile: props.isMobile, isViewMode: props.isViewMode }) }), enableMultipleConditions ? (_jsx("div", { className: "gd-mvf-condition-action", children: idx === 0 ? (_jsx(ConditionActionButton, { icon: "plus", isMobile: props.isMobile, isDisabled: isAddConditionDisabled, onClick: handleAddCondition, dataTestId: "mvf-add-condition", label: addConditionTooltip, tooltip: isAddConditionDisabled
560
560
  ? addConditionDisabledTooltip
561
561
  : addConditionTooltip })) : (_jsx(ConditionActionButton, { icon: "cross", isMobile: props.isMobile, isDestructive: true, onClick: () => handleRemoveCondition(idx), dataTestId: `mvf-remove-condition-${idx}`, label: removeConditionTooltip, tooltip: removeConditionTooltip })) })) : null] }), c.operator === "ALL" ? null : (_jsxs("div", { className: "gd-mvf-condition-inputs", children: [
562
- _jsx(ConditionInputSection, { index: idx, condition: c, usePercentage: props.usePercentage ?? false, baseDisableAutofocus: props.disableAutofocus, separators: props.separators, onValueChange: handleValueChange, onFromChange: handleFromChange, onToChange: handleToChange, onValueBlur: handleValueBlur, onFromBlur: handleFromBlur, onToBlur: handleToBlur, onApply: onApply }), idx ===
562
+ _jsx(ConditionInputSection, { index: idx, conditionNumber: (enableMultipleConditions
563
+ ? state.conditions.length
564
+ : 1) > 1
565
+ ? idx + 1
566
+ : undefined, condition: c, usePercentage: props.usePercentage ?? false, baseDisableAutofocus: props.disableAutofocus, separators: props.separators, onValueChange: handleValueChange, onFromChange: handleFromChange, onToChange: handleToChange, onValueBlur: handleValueBlur, onFromBlur: handleFromBlur, onToBlur: handleToBlur, onApply: onApply }), idx ===
563
567
  (enableMultipleConditions
564
568
  ? state.conditions.length - 1
565
- : 0) && shouldShowTreatNullAsZeroCheckbox ? (_jsx(TreatNullValuesAsZeroCheckbox, { onChange: handleTreatNullAsZeroClicked, checked: enabledTreatNullValuesAsZero, isMobile: props.isMobile, intl: intl })) : null, idx ===
569
+ : 0) && shouldShowTreatNullAsZeroCheckbox ? (_jsx(TreatNullValuesAsZeroCheckbox, { onChange: handleTreatNullAsZeroClicked, checked: enabledTreatNullValuesAsZero, isMobile: props.isMobile, isViewMode: props.isViewMode, intl: intl })) : null, idx ===
566
570
  (enableMultipleConditions
567
571
  ? state.conditions.length - 1
568
572
  : 0) && enableRankingWithMvf ? (_jsxs("label", { className: "input-checkbox-label gd-mvf-apply-on-result-checkbox", "data-testid": "mvf-apply-on-result", children: [
569
573
  _jsx("input", { type: "checkbox", name: "apply-on-result", className: "input-checkbox", checked: applyOnResult, onChange: handleApplyOnResultChange }), _jsx("span", { className: "input-label-text", children: intl.formatMessage({
570
574
  id: "mvf.applyOnResultLabel",
571
575
  }) })
572
- ] })) : null] })), enableMultipleConditions && idx < state.conditions.length - 1 ? (_jsx("div", { className: "gd-mvf-conditions-joiner", children: conditionsJoinerOr })) : null] }, idx))), isDimensionalityEnabled ? (_jsx(DimensionalitySection, { dimensionality: dimensionality, insightDimensionality: insightDimensionality, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, onDimensionalityChange: handleDimensionalityChange, isMigratedFilter: isMigratedFilter })) : null] }), isFilterSummaryEnabled ? (_jsx(PreviewSection, { measureTitle: props.measureTitle, usePercentage: props.usePercentage, separators: separators, format: props.format, useShortFormat: props.useShortFormat, dimensionality: dimensionality, showAllPreview: enableMultipleConditions, conditions: state.conditions.map(({ operator, value }) => ({
576
+ ] })) : null] })), enableMultipleConditions && idx < state.conditions.length - 1 ? (_jsx("div", { className: "gd-mvf-conditions-joiner", children: conditionsJoinerOr })) : null] }, idx))), isDimensionalityEnabled ? (_jsx(DimensionalitySection, { dimensionality: dimensionality, insightDimensionality: insightDimensionality, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, onDimensionalityChange: handleDimensionalityChange, isMigratedFilter: isMigratedFilter })) : null] }), isFilterSummaryEnabled ? (_jsx(PreviewSection, { measureTitle: props.measureTitle, showSimplifiedSummary: showSimplifiedSummary, usePercentage: props.usePercentage, separators: separators, format: props.format, useShortFormat: props.useShortFormat, dimensionality: dimensionality, showAllPreview: enableMultipleConditions, conditions: state.conditions.map(({ operator, value }) => ({
573
577
  operator,
574
578
  value,
575
579
  })) })) : null] })) }), (() => {
@@ -1,14 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2020-2026 GoodData Corporation
3
3
  import { Fragment, memo, useCallback, useEffect, useRef, useState } from "react";
4
- import { useMediaQuery } from "@gooddata/sdk-ui-kit";
4
+ import { useIdPrefixed, useMediaQuery } from "@gooddata/sdk-ui-kit";
5
5
  import { DropdownButton } from "./MeasureValueFilterButton.js";
6
6
  import { MeasureValueFilterDropdown } from "./MeasureValueFilterDropdown.js";
7
7
  /**
8
8
  * @beta
9
9
  */
10
- export const MeasureValueFilter = memo(function MeasureValueFilter({ onCancel = () => { }, filter, measureIdentifier, buttonTitle, buttonSubtitle, buttonTitleExtension, buttonDisabled, measureTitle, usePercentage, warningMessage, locale, separators, format, useShortFormat, displayTreatNullAsZeroOption, treatNullAsZeroDefaultValue, enableOperatorSelection, dimensionality, insightDimensionality, isDimensionalityEnabled, isFilterSummaryEnabled, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, withoutApply, BodyComponent, DropdownActionsComponent, enableMultipleConditions = false, enableRankingWithMvf, onApply, DropdownButtonComponent = DropdownButton, autoOpen, loadMetricDetails, isHeaderEnabled, onChange, alignPoints, fullscreenOnMobile, }) {
10
+ export const MeasureValueFilter = memo(function MeasureValueFilter({ onCancel = () => { }, filter, measureIdentifier, buttonTitle, buttonSubtitle, buttonTitleExtension, buttonDisabled, measureTitle, usePercentage, warningMessage, locale, separators, format, useShortFormat, displayTreatNullAsZeroOption, treatNullAsZeroDefaultValue, enableOperatorSelection, dimensionality, insightDimensionality, isDimensionalityEnabled, isFilterSummaryEnabled, showSimplifiedSummary, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, withoutApply, BodyComponent, DropdownActionsComponent, enableMultipleConditions = false, enableRankingWithMvf, onApply, DropdownButtonComponent = DropdownButton, autoOpen, loadMetricDetails, isHeaderEnabled, onChange, alignPoints, fullscreenOnMobile, isViewMode, }) {
11
11
  const [displayDropdown, setDisplayDropdown] = useState(false);
12
+ const dialogId = useIdPrefixed("mvf-dialog");
12
13
  const buttonRef = useRef(null);
13
14
  const autoOpenedRef = useRef(false);
14
15
  const isMobile = useMediaQuery("mobileDevice");
@@ -33,16 +34,17 @@ export const MeasureValueFilter = memo(function MeasureValueFilter({ onCancel =
33
34
  const toggleDropdown = useCallback(() => {
34
35
  setDisplayDropdown((state) => !state);
35
36
  }, []);
36
- const renderDropdownButton = useCallback((onClickHandler) => (_jsx(DropdownButtonComponent, { onClick: onClickHandler, isActive: displayDropdown, buttonTitle: buttonTitle, buttonSubtitle: buttonSubtitle, buttonTitleExtension: buttonTitleExtension, disabled: buttonDisabled })), [
37
+ const renderDropdownButton = useCallback((onClickHandler) => (_jsx(DropdownButtonComponent, { onClick: onClickHandler, isActive: displayDropdown, buttonTitle: buttonTitle, buttonSubtitle: buttonSubtitle, buttonTitleExtension: buttonTitleExtension, disabled: buttonDisabled, dropdownId: dialogId })), [
37
38
  DropdownButtonComponent,
38
39
  buttonDisabled,
39
40
  buttonSubtitle,
40
41
  buttonTitle,
41
42
  buttonTitleExtension,
43
+ dialogId,
42
44
  displayDropdown,
43
45
  ]);
44
46
  return (_jsxs(Fragment, { children: [
45
- _jsx("div", { ref: buttonRef, children: renderDropdownButton(toggleDropdown) }), displayDropdown ? (_jsx(MeasureValueFilterDropdown, { onApply: handleApply, onChange: onChange, withoutApply: withoutApply, BodyComponent: BodyComponent, DropdownActionsComponent: DropdownActionsComponent, onCancel: handleCancel, filter: filter, measureIdentifier: measureIdentifier, measureTitle: measureTitle, usePercentage: usePercentage, warningMessage: warningMessage, locale: locale, separators: separators, format: format, useShortFormat: useShortFormat, displayTreatNullAsZeroOption: displayTreatNullAsZeroOption, treatNullAsZeroDefaultValue: treatNullAsZeroDefaultValue, enableOperatorSelection: enableOperatorSelection, dimensionality: dimensionality, insightDimensionality: insightDimensionality, isDimensionalityEnabled: isDimensionalityEnabled, isFilterSummaryEnabled: isFilterSummaryEnabled, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, onDimensionalityChange: onDimensionalityChange, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, enableMultipleConditions: enableMultipleConditions, enableRankingWithMvf: enableRankingWithMvf, anchorEl: buttonRef.current ?? undefined, loadMetricDetails: loadMetricDetails, isHeaderEnabled: isHeaderEnabled, alignPoints: alignPoints, fullscreenOnMobile: fullscreenOnMobile,
47
+ _jsx("div", { ref: buttonRef, children: renderDropdownButton(toggleDropdown) }), displayDropdown ? (_jsx(MeasureValueFilterDropdown, { onApply: handleApply, onChange: onChange, withoutApply: withoutApply, BodyComponent: BodyComponent, DropdownActionsComponent: DropdownActionsComponent, onCancel: handleCancel, filter: filter, measureIdentifier: measureIdentifier, measureTitle: measureTitle, usePercentage: usePercentage, warningMessage: warningMessage, locale: locale, separators: separators, format: format, useShortFormat: useShortFormat, displayTreatNullAsZeroOption: displayTreatNullAsZeroOption, treatNullAsZeroDefaultValue: treatNullAsZeroDefaultValue, enableOperatorSelection: enableOperatorSelection, dimensionality: dimensionality, insightDimensionality: insightDimensionality, isDimensionalityEnabled: isDimensionalityEnabled, isFilterSummaryEnabled: isFilterSummaryEnabled, showSimplifiedSummary: showSimplifiedSummary, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, onDimensionalityChange: onDimensionalityChange, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, enableMultipleConditions: enableMultipleConditions, enableRankingWithMvf: enableRankingWithMvf, anchorEl: buttonRef.current ?? undefined, loadMetricDetails: loadMetricDetails, isHeaderEnabled: isHeaderEnabled, alignPoints: alignPoints, fullscreenOnMobile: fullscreenOnMobile, isViewMode: isViewMode, dialogId: dialogId,
46
48
  // Mobile header is the same visual button but dismisses via handleCancel
47
49
  // so host onCancel cleanup (e.g. closing the configuration panel,
48
50
  // clearing autoOpen) runs — toggleDropdown would skip that path.
@@ -11,5 +11,11 @@ export interface IMeasureValueFilterDropdownButtonProps {
11
11
  buttonTitleExtension?: ReactNode;
12
12
  disabled?: boolean;
13
13
  onClick: () => void;
14
+ /**
15
+ * Id of the dropdown dialog this button opens. Wired to `aria-controls` while open.
16
+ *
17
+ * @beta
18
+ */
19
+ dropdownId?: string;
14
20
  }
15
- export declare function DropdownButton({ isActive, buttonTitle, onClick }: IMeasureValueFilterDropdownButtonProps): ReactElement;
21
+ export declare function DropdownButton({ isActive, buttonTitle, onClick, disabled, dropdownId }: IMeasureValueFilterDropdownButtonProps): ReactElement;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import cx from "classnames";
3
- export function DropdownButton({ isActive, buttonTitle, onClick, }) {
3
+ export function DropdownButton({ isActive, buttonTitle, onClick, disabled, dropdownId, }) {
4
4
  const className = cx("gd-mvf-dropdown-button", "s-mvf-dropdown-button", "gd-button", "gd-button-secondary", "button-dropdown", "gd-icon-right", { "gd-icon-navigateup": isActive, "gd-icon-navigatedown": !isActive });
5
- return (_jsx("button", { className: className, onClick: onClick, children: buttonTitle }));
5
+ return (_jsx("button", { className: className, onClick: onClick, disabled: disabled, "aria-haspopup": "dialog", "aria-expanded": isActive, "aria-controls": isActive ? dropdownId : undefined, children: buttonTitle }));
6
6
  }
@@ -16,6 +16,13 @@ export interface IMeasureValueFilterDropdownProps extends IMeasureValueFilterCom
16
16
  * @internal
17
17
  */
18
18
  mobileHeader?: ReactNode;
19
+ /**
20
+ * Stable id assigned to the dropdown dialog so the trigger button can reference it via
21
+ * `aria-controls`.
22
+ *
23
+ * @internal
24
+ */
25
+ dialogId?: string;
19
26
  }
20
27
  /**
21
28
  * @beta
@@ -21,7 +21,7 @@ const getTreatNullAsZeroValue = (filter, treatNullAsZeroDefaultValue, enableMult
21
21
  /**
22
22
  * @beta
23
23
  */
24
- export const MeasureValueFilterDropdown = memo(function MeasureValueFilterDropdown({ filter, onCancel, onApply, onChange, withoutApply, BodyComponent, DropdownActionsComponent, measureIdentifier, measureTitle, usePercentage, warningMessage, locale, anchorEl, separators, format, useShortFormat, displayTreatNullAsZeroOption = false, treatNullAsZeroDefaultValue = false, enableOperatorSelection = true, dimensionality, insightDimensionality, isDimensionalityEnabled, isFilterSummaryEnabled, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, enableMultipleConditions = false, enableRankingWithMvf, loadMetricDetails, isHeaderEnabled, alignPoints, fullscreenOnMobile, mobileHeader, }) {
24
+ export const MeasureValueFilterDropdown = memo(function MeasureValueFilterDropdown({ filter, onCancel, onApply, onChange, withoutApply, BodyComponent, DropdownActionsComponent, measureIdentifier, measureTitle, usePercentage, warningMessage, locale, anchorEl, separators, format, useShortFormat, displayTreatNullAsZeroOption = false, treatNullAsZeroDefaultValue = false, enableOperatorSelection = true, dimensionality, insightDimensionality, isDimensionalityEnabled, isFilterSummaryEnabled, showSimplifiedSummary, catalogDimensionality, loadCatalogDimensionality, onDimensionalityChange, isLoadingCatalogDimensionality, enableMultipleConditions = false, enableRankingWithMvf, loadMetricDetails, isHeaderEnabled, alignPoints, fullscreenOnMobile, mobileHeader, dialogId, isViewMode, }) {
25
25
  const applyOnResult = filter?.measureValueFilter.applyOnResult;
26
26
  const buildFilter = useCallback((conditions, newDimensionality, applyOnResult) => {
27
27
  const effectiveConditions = enableMultipleConditions ? conditions : conditions?.slice(0, 1);
@@ -79,5 +79,5 @@ export const MeasureValueFilterDropdown = memo(function MeasureValueFilterDropdo
79
79
  const handleChange = useCallback((conditions, newDimensionality, applyOnResult) => {
80
80
  onChange?.(buildFilter(conditions, newDimensionality, applyOnResult));
81
81
  }, [onChange, buildFilter]);
82
- return (_jsx(Dropdown, { onApply: handleApply, onChange: onChange ? handleChange : undefined, withoutApply: withoutApply, BodyComponent: BodyComponent, DropdownActionsComponent: DropdownActionsComponent, onCancel: onCancel, operator: (filter && measureValueFilterOperator(filter)) || null, conditions: getConditionsFromFilter(filter, enableMultipleConditions), usePercentage: usePercentage, warningMessage: warningMessage, locale: locale, anchorEl: anchorEl, separators: separators, format: format, useShortFormat: useShortFormat, measureTitle: measureTitle, displayTreatNullAsZeroOption: displayTreatNullAsZeroOption, treatNullAsZeroValue: getTreatNullAsZeroValue(filter, treatNullAsZeroDefaultValue, enableMultipleConditions), enableOperatorSelection: enableOperatorSelection, dimensionality: dimensionality, insightDimensionality: insightDimensionality, isDimensionalityEnabled: isDimensionalityEnabled, isFilterSummaryEnabled: isFilterSummaryEnabled, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, onDimensionalityChange: onDimensionalityChange, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, enableMultipleConditions: enableMultipleConditions, enableRankingWithMvf: enableRankingWithMvf, applyOnResult: applyOnResult, loadMetricDetails: loadMetricDetails, isHeaderEnabled: isHeaderEnabled, alignPoints: alignPoints, fullscreenOnMobile: fullscreenOnMobile, mobileHeader: mobileHeader }));
82
+ return (_jsx(Dropdown, { onApply: handleApply, onChange: onChange ? handleChange : undefined, withoutApply: withoutApply, BodyComponent: BodyComponent, DropdownActionsComponent: DropdownActionsComponent, onCancel: onCancel, operator: (filter && measureValueFilterOperator(filter)) || null, conditions: getConditionsFromFilter(filter, enableMultipleConditions), usePercentage: usePercentage, warningMessage: warningMessage, locale: locale, anchorEl: anchorEl, separators: separators, format: format, useShortFormat: useShortFormat, measureTitle: measureTitle, displayTreatNullAsZeroOption: displayTreatNullAsZeroOption, treatNullAsZeroValue: getTreatNullAsZeroValue(filter, treatNullAsZeroDefaultValue, enableMultipleConditions), enableOperatorSelection: enableOperatorSelection, dimensionality: dimensionality, insightDimensionality: insightDimensionality, isDimensionalityEnabled: isDimensionalityEnabled, isFilterSummaryEnabled: isFilterSummaryEnabled, showSimplifiedSummary: showSimplifiedSummary, catalogDimensionality: catalogDimensionality, loadCatalogDimensionality: loadCatalogDimensionality, onDimensionalityChange: onDimensionalityChange, isLoadingCatalogDimensionality: isLoadingCatalogDimensionality, enableMultipleConditions: enableMultipleConditions, enableRankingWithMvf: enableRankingWithMvf, applyOnResult: applyOnResult, loadMetricDetails: loadMetricDetails, isHeaderEnabled: isHeaderEnabled, alignPoints: alignPoints, fullscreenOnMobile: fullscreenOnMobile, mobileHeader: mobileHeader, dialogId: dialogId, isViewMode: isViewMode }));
83
83
  });
@@ -5,6 +5,7 @@ interface IOperatorDropdownProps {
5
5
  isDisabled?: boolean;
6
6
  isAllOperatorDisabled?: boolean;
7
7
  isMobile?: boolean;
8
+ isViewMode?: boolean;
8
9
  }
9
10
  export declare const OperatorDropdown: import("react").NamedExoticComponent<IOperatorDropdownProps>;
10
11
  export {};
@@ -4,25 +4,35 @@ import { memo, useState } from "react";
4
4
  import cx from "classnames";
5
5
  import { capitalize } from "lodash-es";
6
6
  import { useIntl } from "react-intl";
7
- import { Button } from "@gooddata/sdk-ui-kit";
7
+ import { DropdownButton, useId } from "@gooddata/sdk-ui-kit";
8
8
  import { simplifyText } from "@gooddata/util";
9
9
  import { getOperatorIcon, getOperatorTranslationKey } from "./helpers/measureValueFilterOperator.js";
10
10
  import { OperatorDropdownBody } from "./OperatorDropdownBody.js";
11
11
  export const OperatorDropdown = memo(function OperatorDropdown(props) {
12
12
  const intl = useIntl();
13
13
  const [opened, setOpened] = useState(false);
14
+ const id = useId();
15
+ const buttonId = `mvf-operator-button-${id}`;
16
+ const listboxId = `mvf-operator-listbox-${id}`;
17
+ const labelId = `mvf-operator-label-${id}`;
14
18
  const renderDropdownButton = () => {
15
19
  const { operator, isDisabled } = props;
16
20
  const operatorTranslationKey = getOperatorTranslationKey(operator);
17
21
  const title = capitalize(operatorTranslationKey === undefined
18
22
  ? operator
19
23
  : intl.formatMessage({ id: operatorTranslationKey }));
20
- const buttonClasses = cx("gd-mvf-operator-dropdown-button", "s-mvf-operator-dropdown-button", `s-mvf-operator-dropdown-button-${simplifyText(operator)}`, "gd-button-primary", "gd-button-small", {
21
- "button-dropdown": true,
22
- "is-dropdown-open": opened,
23
- "is-active": opened,
24
- });
25
- return (_jsx(Button, { title: title, className: buttonClasses, value: title, onClick: handleOperatorDropdownButtonClick, iconLeft: `gd-icon-${getOperatorIcon(operator)}`, iconRight: opened ? "gd-icon-navigateup" : "gd-icon-navigatedown", disabled: isDisabled }));
24
+ const buttonClasses = cx("gd-mvf-operator-dropdown-button", "s-mvf-operator-dropdown-button", `s-mvf-operator-dropdown-button-${simplifyText(operator)}`);
25
+ return (_jsx(DropdownButton, { id: buttonId, title: title, value: title, className: buttonClasses, onClick: handleOperatorDropdownButtonClick, iconLeft: `gd-icon-${getOperatorIcon(operator)}`, disabled: isDisabled, isOpen: opened, dropdownId: listboxId, accessibilityConfig: {
26
+ // A plain button that toggles a listbox popup. Using role="button" (rather
27
+ // than the DropdownButton default of "combobox") avoids the WCAG 4.1.2
28
+ // requirement that a combobox always expose aria-controls — our listbox only
29
+ // exists in the DOM while open.
30
+ role: "button",
31
+ popupType: "listbox",
32
+ // Compose the accessible name from the visible "Condition" label and the
33
+ // current operator value rendered inside the button.
34
+ ariaLabelledBy: `${labelId} ${buttonId}`,
35
+ } }));
26
36
  };
27
37
  const handleOperatorSelected = (operator) => {
28
38
  closeOperatorDropdown();
@@ -32,5 +42,5 @@ export const OperatorDropdown = memo(function OperatorDropdown(props) {
32
42
  const handleOperatorDropdownButtonClick = () => setOpened((state) => !state);
33
43
  return (_jsxs(_Fragment, { children: [
34
44
  _jsxs("div", { className: "gd-mvf-operator-dropdown", "data-testid": "mvf-operator-section", children: [
35
- _jsx("div", { className: "gd-mvf-operator-dropdown-label", children: intl.formatMessage({ id: "mvf.condition" }) }), renderDropdownButton()] }), opened ? (_jsx(OperatorDropdownBody, { alignTo: ".gd-mvf-operator-dropdown-button", onSelect: handleOperatorSelected, selectedOperator: props.operator, onClose: closeOperatorDropdown, isAllOperatorDisabled: props.isAllOperatorDisabled, isMobile: props.isMobile })) : null] }));
45
+ _jsx("div", { className: "gd-mvf-operator-dropdown-label", id: labelId, children: intl.formatMessage({ id: "mvf.condition" }) }), renderDropdownButton()] }), opened ? (_jsx(OperatorDropdownBody, { alignTo: `#${buttonId}`, onSelect: handleOperatorSelected, selectedOperator: props.operator, onClose: closeOperatorDropdown, listboxId: listboxId, labelId: labelId, isAllOperatorDisabled: props.isAllOperatorDisabled, isMobile: props.isMobile, isViewMode: props.isViewMode })) : null] }));
36
46
  });
@@ -4,8 +4,13 @@ interface IOperatorDropdownBodyProps {
4
4
  onSelect: (operator: MeasureValueFilterOperator) => void;
5
5
  onClose: () => void;
6
6
  alignTo: string;
7
+ /** id of the listbox, referenced by the trigger button's aria-controls. */
8
+ listboxId: string;
9
+ /** id of the visible "Condition" label, used as the listbox accessible name. */
10
+ labelId?: string;
7
11
  isAllOperatorDisabled?: boolean;
8
12
  isMobile?: boolean;
13
+ isViewMode?: boolean;
9
14
  }
10
15
  export declare const OperatorDropdownBody: import("react").NamedExoticComponent<IOperatorDropdownBodyProps>;
11
16
  export {};
@@ -1,15 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2019-2026 GoodData Corporation
3
- import { Fragment, memo } from "react";
3
+ import { memo, useMemo } from "react";
4
4
  import cx from "classnames";
5
5
  import { capitalize } from "lodash-es";
6
6
  import { defineMessages, useIntl } from "react-intl";
7
- import { FullScreenOverlay, Overlay, Separator } from "@gooddata/sdk-ui-kit";
7
+ import { Bubble, BubbleHoverTrigger, FullScreenOverlay, Overlay, SingleSelectListItem, UiFocusManager, UiListbox, } from "@gooddata/sdk-ui-kit";
8
+ import { simplifyText } from "@gooddata/util";
8
9
  import { MEASURE_VALUE_FILTER_OPERATOR_DROPDOWN_BODY_CLASS } from "./constants.js";
9
- import { getOperatorTranslationKey } from "./helpers/measureValueFilterOperator.js";
10
- import { OperatorDropdownItem } from "./OperatorDropdownItem.js";
10
+ import { getOperatorIcon, getOperatorTranslationKey } from "./helpers/measureValueFilterOperator.js";
11
11
  const MOBILE_DROPDOWN_ALIGN_POINTS = [{ align: "tl tl" }];
12
- // Operators grouped as they appear in the picker; a Separator is rendered between groups
12
+ const DISABLED_BUBBLE_ALIGN_POINTS = [{ align: "cr cl" }, { align: "cl cr" }];
13
+ const DESKTOP_LISTBOX_MAX_HEIGHT = 350;
14
+ // Operators grouped as they appear in the picker; a separator is rendered between groups
13
15
  // (desktop only — mobile rows already carry a bottom divider).
14
16
  const OPERATOR_GROUPS = [
15
17
  ["ALL"],
@@ -30,7 +32,29 @@ const OPERATOR_BUBBLE_MESSAGES = {
30
32
  BETWEEN: bubbleMessages.BETWEEN,
31
33
  NOT_BETWEEN: bubbleMessages.NOT_BETWEEN,
32
34
  };
33
- export const OperatorDropdownBody = memo(function OperatorDropdownBody({ onSelect, onClose, selectedOperator, alignTo, isAllOperatorDisabled = false, isMobile = false, }) {
35
+ /**
36
+ * Renders a single operator as an accessible listbox option (role="option" is supplied by
37
+ * the enclosing {@link UiListbox}). Preserves the legacy `s-mvf-operator-*` hooks, the operator
38
+ * icon, the explanatory bubble for range operators and the disabled-state tooltip. The icon and
39
+ * info bubble are handled by SingleSelectListItem's built-in renderers (which also apply
40
+ * `aria-hidden` to the decorative icon).
41
+ */
42
+ function OperatorListItem({ item, isSelected, isFocused, onSelect, }) {
43
+ const { operator, iconClass, bubbleText, disabledTooltip } = item.data;
44
+ const isDisabled = !!item.isDisabled;
45
+ const listItem = (_jsx(SingleSelectListItem, { className: cx("gd-list-item-shortened", `s-mvf-operator-${simplifyText(operator)}`, {
46
+ "is-disabled": isDisabled,
47
+ }), title: item.stringTitle, icon: iconClass, info: bubbleText, isSelected: isSelected, isFocused: isFocused, onClick: isDisabled ? undefined : onSelect }));
48
+ if (isDisabled && disabledTooltip) {
49
+ return (_jsxs(BubbleHoverTrigger, { tagName: "div", showDelay: 400, hideDelay: 200, children: [listItem, _jsx(Bubble, { className: "bubble-primary", alignPoints: DISABLED_BUBBLE_ALIGN_POINTS, children: disabledTooltip })
50
+ ] }));
51
+ }
52
+ return listItem;
53
+ }
54
+ function OperatorSeparatorItem() {
55
+ return _jsx(SingleSelectListItem, { type: "separator", accessibilityConfig: { role: "separator" } });
56
+ }
57
+ export const OperatorDropdownBody = memo(function OperatorDropdownBody({ onSelect, onClose, selectedOperator, alignTo, listboxId, labelId, isAllOperatorDisabled = false, isMobile = false, isViewMode = false, }) {
34
58
  const intl = useIntl();
35
59
  const allOperatorDisabledTooltip = isAllOperatorDisabled
36
60
  ? intl.formatMessage({ id: "mvf.operator.all.disabled.tooltip" })
@@ -39,18 +63,59 @@ export const OperatorDropdownBody = memo(function OperatorDropdownBody({ onSelec
39
63
  const selectedOperatorTitle = capitalize(selectedOperatorTranslationKey === undefined
40
64
  ? selectedOperator
41
65
  : intl.formatMessage({ id: selectedOperatorTranslationKey }));
42
- const items = (_jsx("div", { className: cx(MEASURE_VALUE_FILTER_OPERATOR_DROPDOWN_BODY_CLASS, "s-mvf-operator-dropdown-body", {
43
- "gd-is-mobile": isMobile,
44
- }), "data-testid": "mvf-operator-dropdown-body", children: OPERATOR_GROUPS.map((group, groupIdx) => (_jsxs(Fragment, { children: [groupIdx > 0 && !isMobile ? _jsx(Separator, {}) : null, group.map((operator) => {
45
- const bubbleMessage = OPERATOR_BUBBLE_MESSAGES[operator];
46
- return (_jsx(OperatorDropdownItem, { operator: operator, selectedOperator: selectedOperator, onClick: onSelect, bubbleText: bubbleMessage ? intl.formatMessage(bubbleMessage) : undefined, isDisabled: operator === "ALL" ? isAllOperatorDisabled : undefined, disabledTooltip: operator === "ALL" ? allOperatorDisabledTooltip : undefined, isMobile: isMobile }, operator));
47
- })] }, groupIdx))) }));
66
+ const items = useMemo(() => {
67
+ const result = [];
68
+ OPERATOR_GROUPS.forEach((group, groupIdx) => {
69
+ if (groupIdx > 0 && !isMobile) {
70
+ result.push({
71
+ type: "static",
72
+ id: `mvf-operator-separator-${groupIdx}`,
73
+ data: null,
74
+ });
75
+ }
76
+ group.forEach((operator) => {
77
+ const translationKey = getOperatorTranslationKey(operator);
78
+ const title = translationKey === undefined ? operator : intl.formatMessage({ id: translationKey });
79
+ const bubbleMessage = OPERATOR_BUBBLE_MESSAGES[operator];
80
+ const isDisabled = operator === "ALL" ? isAllOperatorDisabled : false;
81
+ result.push({
82
+ type: "interactive",
83
+ id: operator,
84
+ stringTitle: capitalize(title),
85
+ isDisabled,
86
+ data: {
87
+ operator,
88
+ iconClass: isMobile ? undefined : `gd-icon-${getOperatorIcon(operator)}`,
89
+ bubbleText: bubbleMessage && !isMobile && !isViewMode
90
+ ? intl.formatMessage(bubbleMessage)
91
+ : undefined,
92
+ disabledTooltip: isDisabled ? allOperatorDisabledTooltip : undefined,
93
+ },
94
+ });
95
+ });
96
+ });
97
+ return result;
98
+ }, [allOperatorDisabledTooltip, intl, isAllOperatorDisabled, isMobile, isViewMode]);
99
+ const ariaAttributes = {
100
+ id: listboxId,
101
+ "aria-labelledby": labelId,
102
+ };
103
+ const body = (
104
+ // Focus moves into the listbox on open and returns to the trigger on close, while Tab
105
+ // stays inside the popup — matching the listbox APG pattern used by the text filter.
106
+ _jsx(UiFocusManager, { enableFocusTrap: true, enableAutofocus: true, enableReturnFocusOnUnmount: true, children: _jsx("div", { className: cx(MEASURE_VALUE_FILTER_OPERATOR_DROPDOWN_BODY_CLASS, "s-mvf-operator-dropdown-body", { "gd-is-mobile": isMobile }), children: _jsx(UiListbox, { shouldKeyboardActionPreventDefault: true, shouldKeyboardActionStopPropagation: true, isDisabledFocusable: true, maxHeight: isMobile ? undefined : DESKTOP_LISTBOX_MAX_HEIGHT, dataTestId: "mvf-operator-dropdown-body",
107
+ // Put the per-operator testid on the role="option" <li> (which carries
108
+ // aria-disabled) rather than the inner item, so disabled-state assertions read
109
+ // the attribute off the element that actually exposes it.
110
+ itemDataTestId: (item) => item.type === "interactive"
111
+ ? `mvf-operator-${simplifyText(item.data.operator)}`
112
+ : undefined, items: items, selectedItemId: selectedOperator, onSelect: (item) => onSelect(item.data.operator), onClose: onClose, ariaAttributes: ariaAttributes, InteractiveItemComponent: OperatorListItem, StaticItemComponent: OperatorSeparatorItem }) }) }));
48
113
  if (isMobile) {
49
114
  return (_jsx(FullScreenOverlay, { alignTo: "body", alignPoints: MOBILE_DROPDOWN_ALIGN_POINTS, onClose: onClose, children: _jsxs("div", { className: "gd-mobile-dropdown-overlay overlay gd-flex-row-container gd-mvf-mobile-dropdown", children: [
50
115
  _jsx("div", { className: "gd-mobile-dropdown-header gd-flex-item gd-mvf-mobile-dropdown-header", children: _jsxs("button", { type: "button", className: "gd-mvf-operator-mobile-header s-mvf-operator-mobile-header", onClick: onClose, children: [
51
116
  _jsx("span", { className: "gd-mvf-operator-mobile-header__label", children: intl.formatMessage({ id: "mvf.condition" }) }), _jsx("span", { className: "gd-mvf-operator-mobile-header__value", children: selectedOperatorTitle }), _jsx("span", { className: "gd-mvf-operator-mobile-header__chevron gd-icon-navigateup" })
52
- ] }) }), _jsx("div", { className: "gd-mobile-dropdown-content gd-flex-item-stretch gd-mvf-mobile-dropdown-content", children: items })
117
+ ] }) }), _jsx("div", { className: "gd-mobile-dropdown-content gd-flex-item-stretch gd-mvf-mobile-dropdown-content", children: body })
53
118
  ] }) }));
54
119
  }
55
- return (_jsx(Overlay, { closeOnOutsideClick: true, alignTo: alignTo, alignPoints: [{ align: "bl tl" }], onClose: onClose, children: _jsx("div", { className: "gd-dropdown overlay", children: items }) }));
120
+ return (_jsx(Overlay, { closeOnOutsideClick: true, alignTo: alignTo, alignPoints: [{ align: "bl tl" }], onClose: onClose, children: _jsx("div", { className: "gd-dropdown overlay", children: body }) }));
56
121
  });
@@ -3,6 +3,11 @@ import { type MeasureValueFilterOperator } from "./types.js";
3
3
  import { type IDimensionalityItem } from "./typings.js";
4
4
  interface IPreviewSectionProps {
5
5
  measureTitle?: string;
6
+ /**
7
+ * When true, renders a simplified summary: the "Preview:" header and the metric title are
8
+ * omitted, so only the conditions (and dimensionality, if any) are shown.
9
+ */
10
+ showSimplifiedSummary?: boolean;
6
11
  usePercentage?: boolean;
7
12
  separators?: ISeparators;
8
13
  format?: string;