Repository: gradio-app/gradio

Issue:
Dataframe "New column" generating error when submitting
### Describe the bug

Hi,

I tried using the "New column" feature to populate custom data in the Dataframe component, but it seems to be giving me an error. When adding a new column and submitting, I get the following error : 

`ValueError: 3 columns passed, passed data had 4 columns`

I tested this feature in the Colab example in the documentation and Gradio also flags an error when submitting.

### Reproduction

```python
import gradio as gr


def filter_records(records, gender):
    return records[records["gender"] == gender]


iface = gr.Interface(
    filter_records,
    [
        gr.inputs.Dataframe(
            headers=["name", "age", "gender"],
            datatype=["str", "number", "str"],
            row_count=5,
        ),
        gr.inputs.Dropdown(["M", "F", "O"]),
    ],
    "dataframe",
    description="Enter gender as 'M', 'F', or 'O' for other.",
)

iface.test_launch()

iface.launch()

```

### Screenshot

<img width="948" alt="image" src="https://user-images.githubusercontent.com/88398271/156010948-8612af41-d492-488b-9a3e-ee83c623056f.png">

### Logs

```shell
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/dist-packages/gradio/routes.py", line 232, in predict
    app.interface.process, raw_input
  File "/usr/local/lib/python3.7/dist-packages/starlette/concurrency.py", line 39, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "/usr/local/lib/python3.7/dist-packages/anyio/to_thread.py", line 29, in run_sync
    limiter=limiter)
  File "/usr/local/lib/python3.7/dist-packages/anyio/_backends/_asyncio.py", line 818, in run_sync_in_worker_thread
    return await future
  File "/usr/local/lib/python3.7/dist-packages/anyio/_backends/_asyncio.py", line 754, in run
    result = context.run(func, *args)
  File "/usr/local/lib/python3.7/dist-packages/gradio/interface.py", line 561, in process
    for i, input_component in enumerate(self.input_components)
  File "/usr/local/lib/python3.7/dist-packages/gradio/interface.py", line 561, in <listcomp>
    for i, input_component in enumerate(self.input_components)
  File "/usr/local/lib/python3.7/dist-packages/gradio/inputs.py", line 1470, in preprocess
    return pd.DataFrame(x, columns=self.headers)
  File "/usr/local/lib/python3.7/dist-packages/pandas/core/frame.py", line 700, in __init__
    dtype,
  File "/usr/local/lib/python3.7/dist-packages/pandas/core/internals/construction.py", line 483, in nested_data_to_arrays
    arrays, columns = to_arrays(data, columns, dtype=dtype)
  File "/usr/local/lib/python3.7/dist-packages/pandas/core/internals/construction.py", line 807, in to_arrays
    content, columns = _finalize_columns_and_data(arr, columns, dtype)
  File "/usr/local/lib/python3.7/dist-packages/pandas/core/internals/construction.py", line 909, in _finalize_columns_and_data
    raise ValueError(err) from err
ValueError: 3 columns passed, passed data had 4 columns
```


### System Info

```shell
Gradio version : 2.8.1
OS : macOS Monterey 12.2.1
Browser : Google Chrome Version 98.0.4758.109
```


### Severity

blocker

Discussion:
abidlabs: Wow took me a while to review all of the fixes and new features. Great work, especially love the drag-and-drop csv!

There are a couple of minor issues that I noticed -- for example, dataframe examples don't adjust the number of rows in the dataframe UI when clicked, which can cause problems if the number of rows in the example < the number of rows in the dataframe UI. But these are pretty minor things we can address post 3.0 launch, overall LGTM.

Pushed a few tweaks to make everything work, and will go ahead and merge so that we can release a new version. 

Patch:
diff --git a/demo/blocks_outputs/run.py b/demo/blocks_outputs/run.py
index eb6193400..c13b8ddeb 100644
--- a/demo/blocks_outputs/run.py
+++ b/demo/blocks_outputs/run.py
@@ -27,6 +27,19 @@ with gr.Blocks() as demo:
         md = gr.Markdown(show_label=False)
         label = gr.Label(show_label=False)
         highlight = gr.HighlightedText(show_label=False)
+        gr.Dataframe(interactive=True, col_count=(3, "fixed"))
+        gr.Dataframe(interactive=True, col_count=4)
+        gr.Dataframe(interactive=True, headers=["One", "Two", "Three", "Four"])
+        gr.Dataframe(
+            interactive=True,
+            headers=["One", "Two", "Three", "Four"],
+            col_count=(4, "fixed"),
+            row_count=(7, "fixed"),
+            default_value=[[1, 2, 3, 4]],
+        )
+        gr.Dataframe(
+            interactive=True, headers=["One", "Two", "Three", "Four"], col_count=4
+        )
         # layout components are static only
         # carousel doesn't work like other components
         # carousel = gr.Carousel()
diff --git a/demo/filter_records/run.py b/demo/filter_records/run.py
index 6f2faf282..92e54b113 100644
--- a/demo/filter_records/run.py
+++ b/demo/filter_records/run.py
@@ -12,6 +12,7 @@ demo = gr.Interface(
             headers=["name", "age", "gender"],
             datatype=["str", "number", "str"],
             row_count=5,
+            col_count=(3, "fixed")
         ),
         gr.Dropdown(["M", "F", "O"]),
     ],
diff --git a/demo/tax_calculator/run.py b/demo/tax_calculator/run.py
index c3d2ff5ad..aebb841b0 100644
--- a/demo/tax_calculator/run.py
+++ b/demo/tax_calculator/run.py
@@ -3,7 +3,7 @@ import gradio as gr
 
 def tax_calculator(income, marital_status, assets):
     tax_brackets = [(10, 0), (25, 8), (60, 12), (120, 20), (250, 30)]
-    total_deductible = sum(assets[assets["Deduct"]]["Cost"])
+    total_deductible = sum(assets["Cost"])
     taxable_income = income - total_deductible
 
     total_tax = 0
@@ -25,15 +25,15 @@ demo = gr.Interface(
         "number",
         gr.Radio(["Single", "Married", "Divorced"]),
         gr.Dataframe(
-            headers=["Item", "Cost", "Deduct"],
-            datatype=["str", "number", "bool"],
+            headers=["Item", "Cost"],
+            datatype=["str", "number"],
             label="Assets Purchased this Year",
         ),
     ],
     "number",
     examples=[
-        [10000, "Married", [["Car", 5000, False], ["Laptop", 800, True]]],
-        [80000, "Single", [["Suit", 800, True], ["Watch", 1800, False]]],
+        [10000, "Married", [["Suit", 5000], ["Laptop", 800], ["Car", 1800]]],
+        [80000, "Single", [["Suit", 800], ["Watch", 1800], ["Car", 800]]],
     ],
 )
 
diff --git a/gradio/components.py b/gradio/components.py
index 3cd810930..4b3df766b 100644
--- a/gradio/components.py
+++ b/gradio/components.py
@@ -1901,10 +1901,9 @@ class Dataframe(Changeable, IOComponent):
         default_value: Optional[List[List[Any]]] = None,
         *,
         headers: Optional[List[str]] = None,
-        row_count: int = 3,
-        col_count: Optional[int] = 3,
+        row_count: int | Tuple[int, str] = (3, "dynamic"),
+        col_count: Optional[int | Tuple[int, str]] = None,
         datatype: str | List[str] = "str",
-        col_width: int | List[int] = None,
         type: str = "pandas",
         label: Optional[str] = None,
         max_rows: Optional[int] = 20,
@@ -1917,10 +1916,9 @@ class Dataframe(Changeable, IOComponent):
         Input Parameters:
         default_value (List[List[Any]]): Default value as a pandas DataFrame. TODO: Add support for default value as a filepath
         headers (List[str]): Header names to dataframe. If None, no headers are shown.
-        row_count (int): Limit number of rows for input.
-        col_count (int): Limit number of columns for input. If equal to 1, return data will be one-dimensional. Ignored if `headers` is provided.
+        row_count (Union[int, Tuple[int, str]]): Limit number of rows for input and decide whether user can create new rows. The first element of the tuple is an `int`, the row count; the second should be 'fixed' or 'dynamic', the new row behaviour. If an `int` is passed the rows default to 'dynamic'
+        col_count (Union[int, Tuple[int, str]]): Limit number of columns for input and decide whether user can create new columns. The first element of the tuple is an `int`, the number of columns; the second should be 'fixed' or 'dynamic', the new column behaviour. If an `int` is passed the columns default to 'dynamic'
         datatype (Union[str, List[str]]): Datatype of values in sheet. Can be provided per column as a list of strings, or for the entire sheet as a single string. Valid datatypes are "str", "number", "bool", and "date".
-        col_width (Union[int, List[int]]): Width of columns in pixels. Can be provided as single value or list of values per column.
         type (str): Type of value to be returned by component. "pandas" for pandas dataframe, "numpy" for numpy array, or "array" for a Python array.
         label (str): component name in interface.
         Output Parameters:
@@ -1929,11 +1927,16 @@ class Dataframe(Changeable, IOComponent):
         max_cols (int): Maximum number of columns to display at once. Set to None for infinite.
         overflow_row_behaviour (str): If set to "paginate", will create pages for overflow rows. If set to "show_ends", will show initial and final rows and truncate middle rows.
         """
+
+        self.row_count = self.__process_counts(row_count)
+        self.col_count = self.__process_counts(
+            col_count, len(headers) if headers else 3
+        )
+
+        self.__validate_headers(headers, self.col_count[0])
+
         self.headers = headers
         self.datatype = datatype
-        self.row_count = row_count
-        self.col_count = len(headers) if headers else col_count
-        self.col_width = col_width
         self.type = type
         self.output_type = "auto"
         default_values = {
@@ -1943,10 +1946,10 @@ class Dataframe(Changeable, IOComponent):
             "date": "01/01/1970",
         }
         column_dtypes = (
-            [datatype] * self.col_count if isinstance(datatype, str) else datatype
+            [datatype] * self.col_count[0] if isinstance(datatype, str) else datatype
         )
         self.test_input = [
-            [default_values[c] for c in column_dtypes] for _ in range(row_count)
+            [default_values[c] for c in column_dtypes] for _ in range(self.row_count[0])
         ]
         self.default_value = (
             default_value if default_value is not None else self.test_input
@@ -1962,7 +1965,6 @@ class Dataframe(Changeable, IOComponent):
             "datatype": self.datatype,
             "row_count": self.row_count,
             "col_count": self.col_count,
-            "col_width": self.col_width,
             "default_value": self.default_value,
             "max_rows": self.max_rows,
             "max_cols": self.max_cols,
@@ -1982,7 +1984,7 @@ class Dataframe(Changeable, IOComponent):
                 return pd.DataFrame(x, columns=self.headers)
             else:
                 return pd.DataFrame(x)
-        if self.col_count == 1:
+        if self.col_count[0] == 1:
             x = [row[0] for row in x]
         if self.type == "numpy":
             return np.array(x)
@@ -2044,6 +2046,24 @@ class Dataframe(Changeable, IOComponent):
                 + ". Please choose from: 'pandas', 'numpy', 'array'."
             )
 
+    @staticmethod
+    def __process_counts(count, default=3):
+        if count is None:
+            return (default, "dynamic")
+        if type(count) == int or type(count) == float:
+            return (int(count), "dynamic")
+        else:
+            return count
+
+    @staticmethod
+    def __validate_headers(headers: List[str] | None, col_count: int):
+        if headers is not None and len(headers) != col_count:
+            raise ValueError(
+                "The length of the headers list must be equal to the col_count int.\nThe column count is set to {cols} but `headers` has {headers} items. Check the values passed to `col_count` and `headers`.".format(
+                    cols=col_count, headers=len(headers)
+                )
+            )
+
 
 class Timeseries(Changeable, IOComponent):
     """
diff --git a/ui/packages/app/src/components/DataFrame/DataFrame.svelte b/ui/packages/app/src/components/DataFrame/DataFrame.svelte
index cc2957099..fb38e70fe 100644
--- a/ui/packages/app/src/components/DataFrame/DataFrame.svelte
+++ b/ui/packages/app/src/components/DataFrame/DataFrame.svelte
@@ -12,6 +12,8 @@
 	export let default_value: Array<Array<string | number>> = [["", "", ""]];
 	export let style: string = "";
 	export let mode: "static" | "dynamic";
+	export let col_count: [number, "fixed" | "dynamic"];
+	export let row_count: [number, "fixed" | "dynamic"];
 
 	if (default_value) value = default_value;
 
@@ -39,6 +41,8 @@
 <div class="relative">
 	<StatusTracker {...loading_status} />
 	<Table
+		{row_count}
+		{col_count}
 		values={value}
 		{headers}
 		{style}
diff --git a/ui/packages/table/package.json b/ui/packages/table/package.json
index b088fcfb1..dec3d8812 100644
--- a/ui/packages/table/package.json
+++ b/ui/packages/table/package.json
@@ -6,5 +6,10 @@
 	"main": "src/index.ts",
 	"author": "",
 	"license": "ISC",
-	"private": true
+	"private": true,
+	"dependencies": {
+		"@gradio/upload": "workspace:^0.0.1",
+		"d3-dsv": "^3.0.1",
+		"dequal": "^2.0.2"
+	}
 }
diff --git a/ui/packages/table/src/EditableCell.svelte b/ui/packages/table/src/EditableCell.svelte
new file mode 100644
index 000000000..5fd2862e9
--- /dev/null
+++ b/ui/packages/table/src/EditableCell.svelte
@@ -0,0 +1,28 @@
+<script lang="ts">
+	export let edit: boolean;
+	export let value: string | number = "";
+	export let el: HTMLInputElement | null;
+	export let header: boolean = false;
+</script>
+
+{#if edit}
+	<input
+		class="absolute flex-1 outline-none inset-2 bg-transparent border-0 translate-x-px flex-1"
+		class:translate-x-px={!header}
+		class:font-bold={header}
+		tabindex="-1"
+		bind:value
+		bind:this={el}
+		on:keydown
+		on:blur={({ currentTarget }) =>
+			currentTarget.setAttribute("tabindex", "-1")}
+	/>
+{/if}
+<span
+	on:dblclick
+	tabindex="-1"
+	role="button"
+	class:opacity-0={edit}
+	class:pointer-events-none={edit}
+	class="p-2  outline-none border-0 flex-1">{value}</span
+>
diff --git a/ui/packages/table/src/Table.svelte b/ui/packages/table/src/Table.svelte
index 3432bca04..ea0e500cd 100644
--- a/ui/packages/table/src/Table.svelte
+++ b/ui/packages/table/src/Table.svelte
@@ -1,8 +1,15 @@
 <script lang="ts">
 	import { createEventDispatcher, tick } from "svelte";
+	import { dsvFormat } from "d3-dsv";
+	import { dequal } from "dequal/lite";
+
+	import { Upload } from "@gradio/upload";
+	import EditableCell from "./EditableCell.svelte";
 
 	export let headers: Array<string> = [];
 	export let values: Array<Array<string | number>> = [[]];
+	export let col_count: [number, "fixed" | "dynamic"];
+	export let row_count: [number, "fixed" | "dynamic"];
 
 	export let style: string = "";
 
@@ -19,55 +26,66 @@
 
 	type Headers = Array<{ value: string; id: string }>;
 
-	function make_headers(_h: Array<string>): Headers {
+	function make_headers(_head: Array<string>): Headers {
+		let _h = _head || [];
+		if (col_count[1] === "fixed" && _h.length < col_count[0]) {
+			const fill = Array(col_count[0] - _h.length)
+				.fill("")
+				.map((v, i) => `${i + _h.length}`);
+			_h = _h.concat(fill);
+		}
+
 		if (!_h || _h.length === 0) {
-			return values[0].map((_, i) => {
-				const _id = `h-${i}`;
-				els[_id] = { cell: null, input: null };
-				return { id: _id, value: JSON.stringify(i + 1) };
-			});
+			return Array(col_count[0])
+				.fill(0)
+				.map((_, i) => {
+					const _id = `h-${i}`;
+					els[_id] = { cell: null, input: null };
+					return { id: _id, value: JSON.stringify(i + 1) };
+				});
 		} else {
 			return _h.map((h, i) => {
 				const _id = `h-${i}`;
 				els[_id] = { cell: null, input: null };
-				return { id: _id, value: h };
+				return { id: _id, value: h || "" };
 			});
 		}
 	}
 
 	function process_data(_values: Array<Array<string | number>>) {
-		return (
-			_values.map((x, i) =>
-				x.map((n, j) => {
-					const id = `${i}-${j}`;
-					els[id] = { input: null, cell: null };
-					return { value: n, id };
-				})
-			) || [
-				Array(headers.length)
+		const data_row_length = _values.length > 0 ? _values.length : row_count[0];
+
+		return Array(
+			row_count[1] === "fixed"
+				? row_count[0]
+				: data_row_length < row_count[0]
+				? row_count[0]
+				: data_row_length
+		)
+			.fill(0)
+			.map((x, i) =>
+				Array(col_count[1] === "fixed" ? col_count[0] : _values[0].length)
 					.fill(0)
-
-					.map((_, j) => {
-						const id = `0-${j}`;
+					.map((n, j) => {
+						const id = `${i}-${j}`;
 						els[id] = { input: null, cell: null };
-
-						return { value: "", id: `0-${j}` };
+						return { value: _values?.[i]?.[j] || "", id };
 					})
-			]
-		);
+			);
 	}
 
 	let _headers = make_headers(headers);
 	let old_headers: Array<string> | undefined;
+
 	$: {
-		if (!is_equal(headers, old_headers)) {
+		if (!dequal(headers, old_headers)) {
 			_headers = make_headers(headers);
+
 			old_headers = headers;
 			refresh_focus();
 		}
 	}
-
-	$: if (!is_equal(values, old_val)) {
+	$: if (!dequal(values, old_val)) {
 		data = process_data(values);
 		old_val = values;
 
@@ -88,23 +106,11 @@
 
 	let old_val: undefined | Array<Array<string | number>> = undefined;
 
-	function is_equal(arr: Array<any>, arr2: Array<any> | undefined) {
-		if (!arr2) return false;
-		return arr.every((_arr, i) => {
-			if (Array.isArray(_arr)) {
-				return _arr.every((item, j) => {
-					return item === arr2?.[i]?.[j];
-				});
-			} else {
-				return _arr === arr2?.[i];
-			}
-		});
-	}
-
-	$: dispatch(
-		"change",
-		data.map((r) => r.map(({ value }) => value))
-	);
+	$: _headers &&
+		dispatch(
+			"change",
+			data.map((r) => r.map(({ value }) => value))
+		);
 
 	function get_sort_status(
 		name: string,
@@ -118,21 +124,39 @@
 		}
 	}
 
-	async function start_edit(id: string) {
-		if (!editable) return;
+	function get_current_indices(id: string) {
+		return data.reduce(
+			(acc, arr, i) => {
+				const j = arr.reduce((acc, data, j) => (id === data.id ? j : acc), -1);
+
+				return j === -1 ? acc : [i, j];
+			},
+			[-1, -1]
+		);
+	}
+
+	async function start_edit(id: string, clear?: boolean) {
+		if (!editable || editing === id) return;
+
+		if (clear) {
+			const [i, j] = get_current_indices(id);
+
+			data[i][j].value = "";
+		}
 		editing = id;
 		await tick();
 		const { input } = els[id];
 		input?.focus();
 	}
 
-	function handle_keydown(
+	async function handle_keydown(
 		event: KeyboardEvent,
 		i: number,
 		j: number,
 		id: string
 	) {
 		let is_data;
+
 		switch (event.key) {
 			case "ArrowRight":
 				if (editing) break;
@@ -161,16 +185,26 @@
 			case "Escape":
 				if (!editable) break;
 				event.preventDefault();
+				selected = editing;
 				editing = false;
 				break;
 			case "Enter":
 				if (!editable) break;
 				event.preventDefault();
-				if (editing === id) {
-					editing = false;
+
+				if (event.shiftKey) {
+					add_row(i);
+					await tick();
+					const [pos] = get_current_indices(id);
+					selected = data[pos + 1][j].id;
 				} else {
-					editing = id;
+					if (editing === id) {
+						editing = false;
+					} else {
+						start_edit(id);
+					}
 				}
+
 				break;
 			case "Backspace":
 				if (!editable) break;
@@ -185,8 +219,29 @@
 					event.preventDefault();
 					data[i][j].value = "";
 				}
+				break;
+			case "Tab":
+				let direction = event.shiftKey ? -1 : 1;
+
+				let is_data_x = data[i][j + direction];
+				let is_data_y =
+					data?.[i + direction]?.[direction > 0 ? 0 : _headers.length - 1];
+				let _selected = is_data_x || is_data_y;
+				if (_selected) {
+					event.preventDefault();
+					selected = _selected ? _selected.id : selected;
+				}
+				editing = false;
+
 				break;
 			default:
+				if (
+					(!editing || (editing && editing !== id)) &&
+					event.key.length === 1
+				) {
+					start_edit(id, true);
+				}
+
 				break;
 		}
 	}
@@ -199,7 +254,7 @@
 	}
 
 	async function set_focus(id: string | boolean, type: "edit" | "select") {
-		if (type === "edit" && typeof id == "number") {
+		if (type === "edit" && typeof id == "string") {
 			await tick();
 			els[id].input?.focus();
 		}
@@ -207,18 +262,18 @@
 		if (
 			type === "edit" &&
 			typeof id == "boolean" &&
-			typeof selected === "number"
+			typeof selected === "string"
 		) {
 			let cell = els[selected]?.cell;
 			await tick();
 			cell?.focus();
 		}
 
-		if (type === "select" && typeof id == "number") {
+		if (type === "select" && typeof id == "string") {
 			const { cell } = els[id];
-			cell?.setAttribute("tabindex", "0");
+			// cell?.setAttribute("tabindex", "0");
 			await tick();
-			els[id].cell?.focus();
+			cell?.focus();
 		}
 	}
 
@@ -255,7 +310,7 @@
 	let header_edit: string | boolean;
 
 	async function edit_header(_id: string, select?: boolean) {
-		if (!editable) return;
+		if (!editable || col_count[1] !== "dynamic" || editing === _id) return;
 		header_edit = _id;
 		await tick();
 		els[_id].input?.focus();
@@ -269,26 +324,36 @@
 		switch (event.key) {
 			case "Escape":
 				event.preventDefault();
+				selected = header_edit;
 				header_edit = false;
+
 				break;
 			case "Enter":
 				event.preventDefault();
+				selected = header_edit;
 				header_edit = false;
 		}
 	}
 
-	function add_row() {
-		data.push(
-			headers.map((_, i) => {
-				const _id = `${data.length}-${i}`;
-				els[_id] = { cell: null, input: null };
-				return { id: _id, value: "" };
-			})
+	function add_row(index?: number) {
+		if (row_count[1] !== "dynamic") return;
+		data.splice(
+			index ? index + 1 : data.length,
+			0,
+			Array(col_count[0])
+				.fill(0)
+				.map((_, i) => {
+					const _id = `${data.length}-${i}`;
+					els[_id] = { cell: null, input: null };
+					return { id: _id, value: "" };
+				})
 		);
+
 		data = data;
 	}
 
 	async function add_col() {
+		if (col_count[1] !== "dynamic") return;
 		for (let i = 0; i < data.length; i++) {
 			const _id = `${i}-${data[i].length}`;
 			els[_id] = { cell: null, input: null };
@@ -307,162 +372,224 @@
 		edit_header(_id, true);
 	}
 
-	const double_click = (
-		node: HTMLElement,
-		{ click, dblclick }: { click: Function; dblclick: Function }
-	) => {
-		let timer: NodeJS.Timeout | undefined;
-
-		function handler(event: MouseEvent) {
-			if (timer) {
-				clearTimeout(timer);
-				timer = undefined;
-				dblclick(event);
-			} else {
-				timer = setTimeout(() => {
-					click(event);
-					timer = undefined;
-				}, 250);
+	function handle_click_outside(event: Event) {
+		if (typeof editing === "string" && els[editing]) {
+			if (
+				els[editing].cell !== event.target &&
+				!els[editing].cell?.contains(event?.target as Node | null)
+			) {
+				header_edit = false;
 			}
 		}
 
-		node.addEventListener("click", handler);
+		if (typeof header_edit === "string" && els[header_edit]) {
+			if (
+				els[header_edit].cell !== event.target &&
+				!els[header_edit].cell?.contains(event.target as Node | null)
+			) {
+				header_edit = false;
+			}
+		}
+	}
+
+	function guess_delimitaor(text: string, possibleDelimiters: Array<string>) {
+		return possibleDelimiters.filter(weedOut);
+
+		function weedOut(delimiter: string) {
+			var cache = -1;
+			return text.split("\n").every(checkLength);
+
+			function checkLength(line: string) {
+				if (!line) {
+					return true;
+				}
+
+				var length = line.split(delimiter).length;
+				if (cache < 0) {
+					cache = length;
+				}
+				return cache === length && length > 1;
+			}
+		}
+	}
+
+	function data_uri_to_blob(data_uri: string) {
+		const byte_str = atob(data_uri.split(",")[1]);
+		const mime_str = data_uri.split(",")[0].split(":")[1].split(";")[0];
+
+		const ab = new ArrayBuffer(byte_str.length);
+		const ia = new Uint8Array(ab);
+
+		for (let i = 0; i < byte_str.length; i++) {
+			ia[i] = byte_str.charCodeAt(i);
+		}
+
+		return new Blob([ab], { type: mime_str });
+	}
+
+	function blob_to_string(blob: Blob) {
+		const reader = new FileReader();
+
+		function handle_read(e) {
+			const [delimiter] = guess_delimitaor(e.srcElement.result, [",", "\t"]);
+
+			const [head, ...rest] = dsvFormat(delimiter).parseRows(
+				e.srcElement.result
+			);
+
+			_headers = make_headers(
+				col_count[1] === "fixed" ? head.slice(0, col_count[0]) : head
+			);
+
+			values = rest;
+			reader.removeEventListener("loadend", handle_read);
+		}
 
-		return {
-			destroy: () => node.removeEventListener("click", handler)
-		};
-	};
+		reader.addEventListener("loadend", handle_read);
+
+		reader.readAsText(blob);
+	}
+
+	let dragging = false;
 </script>
 
-<div class="overflow-hidden rounded-lg relative border">
-	<table class="table-auto font-mono w-full text-gray-900 text-sm">
-		<thead class="sticky top-0 left-0 right-0 bg-white shadow-sm z-10">
-			<tr class="border-b divide-x dark:divide-gray-800 text-left">
-				{#each _headers as { value, id }, i (id)}
-					<th class="p-0 relative">
-						<div
-							class="flex outline-none focus-within:ring-1 ring-orange-500 focus-within:bg-orange-50 ring-inset {i ===
-							0
-								? 'rounded-tl-lg'
-								: i === _headers.length - 1
-								? 'rounded-tr-lg'
-								: ''}"
+<svelte:window on:click={handle_click_outside} />
+
+<div
+	class="overflow-hidden rounded-lg relative border transition-colors"
+	class:border-green-400={dragging}
+>
+	<Upload
+		center={false}
+		boundedheight={false}
+		click={false}
+		on:load={(e) => blob_to_string(data_uri_to_blob(e.detail.data))}
+		bind:dragging
+	>
+		<table
+			class="table-auto font-mono w-full text-gray-900 text-sm transition-opacity"
+			class:opacity-40={dragging}
+		>
+			<thead class="sticky top-0 left-0 right-0 bg-white shadow-sm z-10">
+				<tr class="border-b divide-x dark:divide-gray-800 text-left">
+					{#each _headers as { value, id }, i (id)}
+						<th
+							bind:this={els[id].cell}
+							class="p-0 relative focus-within:ring-1 ring-orange-500 ring-inset outline-none "
+							class:bg-orange-50={header_edit === id}
+							class:rounded-tl-lg={i === 0}
+							class:rounded-tr-lg={i === _headers.length - 1}
+							aria-sort={get_sort_status(value, sort_by, sort_direction)}
 						>
-							<div
-								class="py-2 pl-2 flex-1 outline-none"
-								contenteditable={editable}
-								on:input={(e) => (value = e.target.innerText)}
-							>
-								{value}
-							</div>
-							<div
-								class="flex items-center justify-center p-2 cursor-pointer !visible leading-snug transform transition-all {sort_by !==
-								i
-									? 'text-gray-200 hover:text-gray-500'
-									: 'text-orange-500'} {sort_by === i &&
-								sort_direction === 'des'
-									? '-scale-y-[1]'
-									: ''}"
-								class:text-gray-200={sort_by !== i}
-								on:click={() => handle_sort(i)}
-							>
-								<svg
-									width="1em"
-									height="1em"
-									class="fill-current text-[10px]"
-									viewBox="0 0 9 7"
-									fill="none"
-									xmlns="http://www.w3.org/2000/svg"
+							<div class="min-h-[2.3rem] flex outline-none">
+								<EditableCell
+									{value}
+									bind:el={els[id].input}
+									edit={header_edit === id}
+									on:keydown={end_header_edit}
+									on:dblclick={() => edit_header(id)}
+									header
+								/>
+
+								<div
+									class="flex flex-none items-center justify-center p-2 cursor-pointer  leading-snug transform transition-all {sort_by !==
+									i
+										? 'text-gray-200 hover:text-gray-500'
+										: 'text-orange-500'} {sort_by === i &&
+									sort_direction === 'des'
+										? '-scale-y-[1]'
+										: ''}"
+									class:text-gray-200={sort_by !== i}
+									on:click={() => handle_sort(i)}
 								>
-									<path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
-								</svg>
+									<svg
+										width="1em"
+										height="1em"
+										class="fill-current text-[10px]"
+										viewBox="0 0 9 7"
+										fill="none"
+										xmlns="http://www.w3.org/2000/svg"
+									>
+										<path d="M4.49999 0L8.3971 6.75H0.602875L4.49999 0Z" />
+									</svg>
+								</div>
 							</div>
-						</div>
-					</th>
-				{/each}
-			</tr>
-		</thead>
-
-		<tbody class="overflow-scroll">
-			{#each data as row, i (row)}
-				<tr
-					class="group border-b last:border-none divide-x dark:divide-gray-800 space-x-4 odd:bg-gray-50 dark:odd:bg-gray-900 group focus:bg-gradient-to-b focus:from-blue-100 dark:focus:from-blue-900 focus:to-blue-50 dark:focus:to-gray-900 focus:odd:bg-white"
-				>
-					{#each row as { value, id }, j (id)}
-						<td
-							class="p-2 outline-none focus-within:ring-1 ring-orange-500 ring-inset focus-within:bg-orange-50 group-last:first:rounded-bl-lg group-last:last:rounded-br-lg"
-							contenteditable={editable}
-							on:input={(e) => (value = e.target.innerText)}
-						>
-							<div
-								class:border-transparent={selected !== id}
-								class="min-h-[3.3rem] px-5 py-3  border-[0.125rem] {selected ===
-								id
-									? 'border-gray-600 dark:border-gray-200'
-									: ''}"
+						</th>
+					{/each}
+				</tr>
+			</thead>
+
+			<tbody class="overflow-scroll">
+				{#each data as row, i (row)}
+					<tr
+						class="group border-b last:border-none divide-x dark:divide-gray-800 space-x-4 odd:bg-gray-50 dark:odd:bg-gray-900 group focus:bg-gradient-to-b focus:from-blue-100 dark:focus:from-blue-900 focus:to-blue-50 dark:focus:to-gray-900 focus:odd:bg-white"
+					>
+						{#each row as { value, id }, j (id)}
+							<td
+								tabindex="0"
+								bind:this={els[id].cell}
+								on:click={() => handle_cell_click(id)}
+								on:dblclick={() => start_edit(id)}
+								on:keydown={(e) => handle_keydown(e, i, j, id)}
+								class=" outline-none focus-within:ring-1 ring-orange-500 ring-inset focus-within:bg-orange-50 group-last:first:rounded-bl-lg group-last:last:rounded-br-lg relative"
 							>
-								{#if editing === id}
-									<input
-										class="outline-none absolute p-0 w-3/4"
-										tabindex="-1"
+								<div
+									class:border-transparent={selected !== id}
+									class="min-h-[2.3rem] h-full  outline-none flex items-center"
+								>
+									<EditableCell
 										bind:value
-										bind:this={els[id].input}
-										on:blur={({ currentTarget }) =>
-											currentTarget.setAttribute("tabindex", "-1")}
+										bind:el={els[id].input}
+										edit={editing === id}
 									/>
-								{/if}
-								<span
-									class="cursor-default w-full"
-									class:opacity-0={editing === id}
-									tabindex="-1"
-									role="button"
-								>
-									{value ?? ""}
-								</span>
-							</div>
-						</td>
-					{/each}
-				</tr>
-			{/each}
-		</tbody>
-	</table>
+								</div>
+							</td>
+						{/each}
+					</tr>
+				{/each}
+			</tbody>
+		</table>
+	</Upload>
 </div>
 {#if editable}
 	<div class="flex justify-end space-x-1 pt-2 text-gray-800">
-		<button class="!flex-none gr-button group" on:click={add_row}
-			><svg
-				xmlns="http://www.w3.org/2000/svg"
-				xmlns:xlink="http://www.w3.org/1999/xlink"
-				aria-hidden="true"
-				role="img"
-				class="mr-1 group-hover:text-orange-500"
-				width="1em"
-				height="1em"
-				preserveAspectRatio="xMidYMid meet"
-				viewBox="0 0 32 32"
-				><path
-					fill="currentColor"
-					d="M24.59 16.59L17 24.17V4h-2v20.17l-7.59-7.58L6 18l10 10l10-10l-1.41-1.41z"
-				/></svg
-			>New row</button
-		>
-		<button class="!flex-none gr-button group" on:click={add_col}>
-			<svg
-				xmlns="http://www.w3.org/2000/svg"
-				xmlns:xlink="http://www.w3.org/1999/xlink"
-				aria-hidden="true"
-				role="img"
-				class="mr-1 group-hover:text-orange-500"
-				width="1em"
-				height="1em"
-				preserveAspectRatio="xMidYMid meet"
-				viewBox="0 0 32 32"
-				><path
-					fill="currentColor"
-					d="m18 6l-1.43 1.393L24.15 15H4v2h20.15l-7.58 7.573L18 26l10-10L18 6z"
-				/></svg
+		{#if row_count[1] === "dynamic"}
+			<button class="!flex-none gr-button group" on:click={() => add_row()}
+				><svg
+					xmlns="http://www.w3.org/2000/svg"
+					xmlns:xlink="http://www.w3.org/1999/xlink"
+					aria-hidden="true"
+					role="img"
+					class="mr-1 group-hover:text-orange-500"
+					width="1em"
+					height="1em"
+					preserveAspectRatio="xMidYMid meet"
+					viewBox="0 0 32 32"
+					><path
+						fill="currentColor"
+						d="M24.59 16.59L17 24.17V4h-2v20.17l-7.59-7.58L6 18l10 10l10-10l-1.41-1.41z"
+					/></svg
+				>New row</button
 			>
-			New column</button
-		>
+		{/if}
+		{#if col_count[1] === "dynamic"}
+			<button class="!flex-none gr-button group" on:click={add_col}>
+				<svg
+					xmlns="http://www.w3.org/2000/svg"
+					xmlns:xlink="http://www.w3.org/1999/xlink"
+					aria-hidden="true"
+					role="img"
+					class="mr-1 group-hover:text-orange-500"
+					width="1em"
+					height="1em"
+					preserveAspectRatio="xMidYMid meet"
+					viewBox="0 0 32 32"
+					><path
+						fill="currentColor"
+						d="m18 6l-1.43 1.393L24.15 15H4v2h20.15l-7.58 7.573L18 26l10-10L18 6z"
+					/></svg
+				>
+				New column</button
+			>{/if}
 	</div>
 {/if}
diff --git a/ui/packages/upload/src/Upload.svelte b/ui/packages/upload/src/Upload.svelte
index ac133b10f..2fa4ac198 100644
--- a/ui/packages/upload/src/Upload.svelte
+++ b/ui/packages/upload/src/Upload.svelte
@@ -8,6 +8,9 @@
 	export let include_file_metadata = true;
 	export let dragging = false;
 	export let style: string = "";
+	export let boundedheight: boolean = true;
+	export let click: boolean = true;
+	export let center: boolean = true;
 
 	let hidden_upload: HTMLInputElement;
 	let file_count: "multiple" | "directory" | "single";
@@ -19,6 +22,7 @@
 	};
 
 	const openFileUpload = () => {
+		if (!click) return;
 		hidden_upload.click();
 	};
 
@@ -64,7 +68,10 @@
 </script>
 
 <div
-	class="w-full cursor-pointer h-full flex items-center justify-center text-center text-gray-400 md:text-xl max-h-[15rem] xl:max-h-[18rem] 2xl:max-h-[20rem] min-h-[10rem] md:min-h-[15rem]"
+	class="w-full cursor-pointer h-full flex items-center justify-center text-gray-400 md:text-xl {boundedheight
+		? 'min-h-[10rem] md:min-h-[15rem] max-h-[15rem] xl:max-h-[18rem] 2xl:max-h-[20rem]'
+		: ''}"
+	class:text-center={center}
 	{theme}
 	on:drag|preventDefault|stopPropagation
 	on:dragstart|preventDefault|stopPropagation
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 800f2df8b..4b2d3cbc4 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -232,7 +232,14 @@ importers:
       plotly.js-dist-min: 2.11.1
 
   packages/table:
-    specifiers: {}
+    specifiers:
+      '@gradio/upload': workspace:^0.0.1
+      d3-dsv: ^3.0.1
+      dequal: ^2.0.2
+    dependencies:
+      '@gradio/upload': link:../upload
+      d3-dsv: 3.0.1
+      dequal: 2.0.2
 
   packages/tabs:
     specifiers: {}
@@ -1941,6 +1948,11 @@ packages:
     dev: false
     optional: true
 
+  /dequal/2.0.2:
+    resolution: {integrity: sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==}
+    engines: {node: '>=6'}
+    dev: false
+
   /detect-indent/6.1.0:
     resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
     engines: {node: '>=8'}


Test patch:
diff --git a/test/test_components.py b/test/test_components.py
index f89b5ba0c..c1c175e0a 100644
--- a/test/test_components.py
+++ b/test/test_components.py
@@ -860,9 +860,8 @@ class TestDataframe(unittest.TestCase):
             {
                 "headers": ["Name", "Age", "Member"],
                 "datatype": "str",
-                "row_count": 3,
-                "col_count": 3,
-                "col_width": None,
+                "row_count": (3, "dynamic"),
+                "col_count": (3, "dynamic"),
                 "default_value": [
                     ["", "", ""],
                     ["", "", ""],
@@ -913,9 +912,8 @@ class TestDataframe(unittest.TestCase):
                 "label": None,
                 "css": {},
                 "datatype": "str",
-                "row_count": 3,
-                "col_count": 3,
-                "col_width": None,
+                "row_count": (3, "dynamic"),
+                "col_count": (3, "dynamic"),
                 "default_value": [
                     ["", "", ""],
                     ["", "", ""],