Line data Source code
1 : import * as React from 'react';
2 :
3 : import * as ActionList from '../../ui/foundations/action_list.js';
4 : import { AnchorAlignment, AnchorSide } from '../../ui/foundations/anchored_position.js';
5 : import { AnchoredOverlay } from '../../ui/foundations/anchored_overlay.js';
6 : import { Button, ButtonSize, ButtonVariant } from '../../ui/foundations/button.js';
7 : import { TriangleDownIcon } from '../../ui/foundations/symbol_icon.js';
8 : import * as styles from './chart_builder.module.css';
9 :
10 : export interface ChartSelectOption {
11 : value: string;
12 : label: string;
13 : description?: string;
14 : }
15 :
16 : interface Props {
17 : /// The accessible name of the trigger
18 : 'aria-label': string;
19 : 'data-testid'?: string;
20 : /// The picked value, null shows the placeholder
21 : value: string | null;
22 : /// The label of the trigger when nothing is picked
23 : placeholder: string;
24 : options: readonly ChartSelectOption[];
25 : onChange: (value: string) => void;
26 : /// The trigger shows a label that differs from the label of the option, e.g. `sum(revenue)`
27 : triggerLabel?: string;
28 : variant?: ButtonVariant;
29 : className?: string;
30 : disabled?: boolean;
31 : }
32 :
33 : /// A dropdown of the chart builder: a button that opens a list of options
34 1 : export function ChartSelect(props: Props): React.ReactElement {
35 1 : const [open, setOpen] = React.useState(false);
36 1 : const triggerRef = React.useRef<HTMLButtonElement>(null);
37 1 : const picked = props.options.find(option => option.value === props.value);
38 1 : const label = props.triggerLabel ?? picked?.label ?? props.value ?? props.placeholder;
39 1 : return (
40 1 : <AnchoredOverlay
41 1 : open={open}
42 1 : onOpen={() => setOpen(true)}
43 1 : onClose={() => setOpen(false)}
44 1 : side={AnchorSide.OutsideBottom}
45 1 : align={AnchorAlignment.Start}
46 1 : anchorOffset={4}
47 1 : anchorRef={triggerRef}
48 1 : returnFocusRef={triggerRef}
49 1 : renderAnchor={anchorProps => (
50 1 : <Button
51 1 : {...anchorProps}
52 1 : ref={triggerRef}
53 1 : className={props.className}
54 1 : variant={props.variant ?? ButtonVariant.Default}
55 1 : size={ButtonSize.Small}
56 1 : disabled={props.disabled}
57 1 : trailingVisual={TriangleDownIcon}
58 1 : aria-label={`${props['aria-label']}: ${label}`}
59 1 : data-testid={props['data-testid']}
60 1 : data-placeholder={props.value == null ? '' : undefined}
61 1 : title={label}
62 : >
63 1 : <span className={styles.select_label}>{label}</span>
64 1 : </Button>
65 : )}
66 : >
67 1 : <ActionList.List className={styles.select_list} aria-label={props['aria-label']} role="listbox">
68 1 : {props.options.map(option => (
69 1 : <ActionList.ListItem
70 1 : key={option.value}
71 1 : role="option"
72 1 : selected={option.value === props.value}
73 1 : data-item={option.value}
74 1 : onClick={() => {
75 1 : setOpen(false);
76 1 : if (option.value !== props.value) props.onChange(option.value);
77 1 : }}
78 : >
79 1 : <ActionList.ItemText>
80 1 : <ActionList.ItemTextTitle>{option.label}</ActionList.ItemTextTitle>
81 1 : {option.description != null ? <ActionList.ItemTextDescription>{option.description}</ActionList.ItemTextDescription> : undefined}
82 1 : </ActionList.ItemText>
83 1 : </ActionList.ListItem>
84 1 : ))}
85 1 : </ActionList.List>
86 1 : </AnchoredOverlay>
87 : );
88 1 : }
|