@marimo-team/islands 0.25.3-dev42 → 0.25.3-dev46

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev42",
3
+ "version": "0.25.3-dev46",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -914,6 +914,94 @@ describe("ExportActions dialog", () => {
914
914
  expect(within(dialog).queryByRole("group")).not.toBeInTheDocument();
915
915
  });
916
916
 
917
+ it("describes format options with their description and current settings", async () => {
918
+ renderExportActions({
919
+ getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
920
+ });
921
+ await openDialog();
922
+ await screen.findByRole("button", { name: "GeoParquet options" });
923
+
924
+ for (const [label, description] of [
925
+ ["CSV", "Comma-separated values Comma · UTF-8"],
926
+ ["TSV", "Best for Excel and Google Sheets UTF-8"],
927
+ ["JSON", "Raw JSON data Escapes non-ASCII"],
928
+ ["GeoParquet", "Parquet with geometry and CRS metadata location"],
929
+ ["GeoJSON", "Geometry in longitude and latitude location"],
930
+ ]) {
931
+ expect(
932
+ screen.getByRole("button", { name: `${label} options` }),
933
+ ).toHaveAccessibleDescription(description);
934
+ }
935
+
936
+ const csvOptions = screen.getByRole("button", { name: "CSV options" });
937
+ fireEvent.click(csvOptions);
938
+ fireEvent.change(screen.getByRole("combobox", { name: "Delimiter" }), {
939
+ target: { value: ";" },
940
+ });
941
+ expect(csvOptions).toHaveAccessibleDescription(
942
+ "Comma-separated values Semicolon · UTF-8",
943
+ );
944
+ fireEvent.click(csvOptions);
945
+ expect(csvOptions).toHaveAccessibleDescription(
946
+ "Comma-separated values Semicolon · UTF-8",
947
+ );
948
+ });
949
+
950
+ it.each([
951
+ ["CSV", "Comma-separated values"],
952
+ ["TSV", "Best for Excel and Google Sheets"],
953
+ ["JSON", "Raw JSON data"],
954
+ ["GeoParquet", "Parquet with geometry and CRS metadata"],
955
+ ["GeoJSON", "Geometry in longitude and latitude"],
956
+ ])(
957
+ "toggles %s options from the row label and description",
958
+ async (label, description) => {
959
+ renderExportActions({
960
+ getExportMetadata: vi.fn().mockResolvedValue(geometryMetadata),
961
+ });
962
+ await openDialog();
963
+ const toggle = await screen.findByRole("button", {
964
+ name: `${label} options`,
965
+ });
966
+
967
+ fireEvent.click(screen.getByText(label));
968
+ expect(toggle).toHaveAttribute("aria-expanded", "true");
969
+ expect(
970
+ screen.getByRole("group", { name: `${label} options` }),
971
+ ).toBeInTheDocument();
972
+
973
+ fireEvent.click(screen.getByText(description));
974
+ expect(toggle).toHaveAttribute("aria-expanded", "false");
975
+ expect(
976
+ screen.queryByRole("group", { name: `${label} options` }),
977
+ ).not.toBeInTheDocument();
978
+ },
979
+ );
980
+
981
+ it("keeps download and copy independent of row expansion", async () => {
982
+ vi.stubGlobal(
983
+ "fetch",
984
+ vi.fn().mockResolvedValue({
985
+ ok: true,
986
+ text: vi.fn().mockResolvedValue("name,value\n"),
987
+ }),
988
+ );
989
+ renderExportActions();
990
+ await openDialog();
991
+ const toggle = screen.getByRole("button", { name: "CSV options" });
992
+
993
+ fireEvent.click(screen.getByRole("button", { name: "Download CSV" }));
994
+ await waitFor(() => expect(downloadByURL).toHaveBeenCalled());
995
+ expect(toggle).toHaveAttribute("aria-expanded", "false");
996
+
997
+ fireEvent.click(screen.getByText("CSV"));
998
+ fireEvent.click(screen.getByRole("button", { name: "Copy CSV" }));
999
+ expect(toggle).toHaveAttribute("aria-expanded", "true");
1000
+ await waitFor(() => {
1001
+ expect(copyToClipboard).toHaveBeenCalledWith("name,value\n");
1002
+ });
1003
+ });
1004
+
917
1005
  it("sends only changed CSV options with a download", async () => {
918
1006
  renderExportActions();
919
1007
  await openDialog();
@@ -761,7 +761,10 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
761
761
  const expanded =
762
762
  configurableFormat !== null &&
763
763
  expandedFormat === configurableFormat;
764
+ const FormatHeader = configurableFormat ? "button" : "div";
764
765
  const panelId = `export-options-${option.format}`;
766
+ const descriptionId = `${panelId}-description`;
767
+ const summaryId = `${panelId}-summary`;
765
768
  const rowFailure =
766
769
  failure?.action.format === option.format ? failure : null;
767
770
  const summary =
@@ -802,55 +805,78 @@ export const ExportActions: React.FC<ExportActionProps> = (props) => {
802
805
  data-testid={`export-row-${option.format}`}
803
806
  className={cn(expanded && "bg-muted/40")}
804
807
  >
805
- <div className="grid min-h-[60px] grid-cols-[28px_minmax(0,1fr)_auto] items-center gap-x-1 px-2.5 py-2 hover:bg-accent/50">
806
- {configurableFormat ? (
807
- <Button
808
- type="button"
809
- variant="ghost"
810
- size="icon"
811
- aria-label={`${optionLabel} options`}
812
- aria-expanded={expanded}
813
- aria-controls={panelId}
814
- onClick={() => toggleExpanded(option.format)}
808
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center hover:bg-accent/50">
809
+ <FormatHeader
810
+ type={configurableFormat ? "button" : undefined}
811
+ className={cn(
812
+ "grid min-h-[60px] min-w-0 grid-cols-[28px_minmax(0,1fr)] items-center gap-x-1 py-2 pl-2.5 pr-1 text-left",
813
+ configurableFormat &&
814
+ "cursor-pointer focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
815
+ )}
816
+ aria-label={
817
+ configurableFormat ? `${optionLabel} options` : undefined
818
+ }
819
+ aria-describedby={
820
+ configurableFormat
821
+ ? [descriptionId, summary && summaryId]
822
+ .filter(Boolean)
823
+ .join(" ")
824
+ : undefined
825
+ }
826
+ aria-expanded={configurableFormat ? expanded : undefined}
827
+ aria-controls={configurableFormat ? panelId : undefined}
828
+ onClick={
829
+ configurableFormat
830
+ ? () => toggleExpanded(option.format)
831
+ : undefined
832
+ }
833
+ >
834
+ <span
835
+ aria-hidden={true}
836
+ className="inline-flex h-6 w-6 items-center justify-center"
815
837
  >
816
- <ChevronRightIcon
817
- className={cn(
818
- "h-4 w-4 text-muted-foreground transition-transform",
819
- expanded && "rotate-90",
820
- )}
838
+ {configurableFormat && (
839
+ <ChevronRightIcon
840
+ className={cn(
841
+ "h-4 w-4 text-muted-foreground transition-transform",
842
+ expanded && "rotate-90",
843
+ )}
844
+ />
845
+ )}
846
+ </span>
847
+ <span className="flex min-w-0 items-center gap-3 pl-1">
848
+ <option.icon
849
+ aria-hidden={true}
850
+ data-testid={`export-format-icon-${option.format}`}
851
+ className="h-4 w-4 shrink-0 text-muted-foreground"
821
852
  />
822
- </Button>
823
- ) : (
824
- <span aria-hidden={true} className="h-6 w-6" />
825
- )}
826
- <div className="flex min-w-0 items-center gap-3 pl-1">
827
- <option.icon
828
- aria-hidden={true}
829
- data-testid={`export-format-icon-${option.format}`}
830
- className="h-4 w-4 shrink-0 text-muted-foreground"
831
- />
832
- <div className="grid min-w-0 gap-1">
833
- <span className="flex min-w-0 items-baseline gap-x-2">
834
- <span className="text-sm font-medium leading-tight">
835
- {optionLabel}
836
- </span>
837
- {summary && (
838
- <span
839
- data-testid={`export-summary-${option.format}`}
840
- className="min-w-0 truncate text-xs text-muted-foreground"
841
- >
842
- {summary}
853
+ <span className="grid min-w-0 gap-1">
854
+ <span className="flex min-w-0 items-baseline gap-x-2">
855
+ <span className="text-sm font-medium leading-tight">
856
+ {optionLabel}
843
857
  </span>
844
- )}
845
- </span>
846
- <span className="text-xs text-muted-foreground leading-tight">
847
- {option.format === "parquet" && hasGeometry
848
- ? "Parquet with geometry and CRS metadata"
849
- : option.description}
858
+ {summary && (
859
+ <span
860
+ id={summaryId}
861
+ data-testid={`export-summary-${option.format}`}
862
+ className="min-w-0 truncate text-xs text-muted-foreground"
863
+ >
864
+ {summary}
865
+ </span>
866
+ )}
867
+ </span>
868
+ <span
869
+ id={descriptionId}
870
+ className="text-xs text-muted-foreground leading-tight"
871
+ >
872
+ {option.format === "parquet" && hasGeometry
873
+ ? "Parquet with geometry and CRS metadata"
874
+ : option.description}
875
+ </span>
850
876
  </span>
851
- </div>
852
- </div>
853
- <div className="flex items-center gap-0.5">
877
+ </span>
878
+ </FormatHeader>
879
+ <div className="flex items-center gap-0.5 pr-2.5">
854
880
  <Tooltip
855
881
  content={
856
882
  option.canDownload