Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/458 document search tests #199

Merged
merged 9 commits into from
Jan 10, 2025
21 changes: 21 additions & 0 deletions app/client/public/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@
max-height: 90vh;
}

.scroll-container {
overflow: auto; /* Ensure the scrollbar is present */
scrollbar-width: thin; /* For Firefox, make the scrollbar thin */
scrollbar-color: darkgray; /* Thumb and track colors */
}

/* For WebKit browsers */
.scroll-container::-webkit-scrollbar {
width: 12px; /* Width of the scrollbar */
height: 12px; /* Height of the scrollbar for horizontal scrolling */
}

.scroll-container::-webkit-scrollbar-thumb {
background-color: darkgray; /* Color of the scrollbar thumb */
border-radius: 6px; /* Round edges */
}

.scrollbar {
height: 12px;
}

.sr-only {
border: 0;
clip: rect(0, 0, 0, 0);
Expand Down
1 change: 1 addition & 0 deletions app/client/src/components/previewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export function PreviewModal<D extends QueryData>({
</small>
)}
<Table
className="margin-top-2"
columns={tableColumns}
data={preview.data}
id={`${id}-table`}
Expand Down
223 changes: 131 additions & 92 deletions app/client/src/components/table.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/** Adapted from https://github.com/MetroStar/comet/blob/main/packages/comet-uswds/src/components/table/table.tsx */
import table from '@uswds/uswds/js/usa-table';
import classNames from 'classnames';
import { useState } from 'react';
import { useRef, useState } from 'react';
// types
import type { ReactNode } from 'react';
import type { ReactNode, UIEvent } from 'react';

function isCellSpec(value: any): value is TableCell {
return (
Expand All @@ -27,6 +27,16 @@ export const Table = ({
className,
tabIndex = -1,
}: TableProps): React.ReactElement => {
const topScrollbarRef = useRef<HTMLDivElement | null>(null);
const contentRef = useRef<HTMLDivElement | null>(null);

const handleScroll = (e: UIEvent<HTMLDivElement>) => {
const scrollLeft = e.currentTarget.scrollLeft;
if (contentRef.current) contentRef.current.scrollLeft = scrollLeft;
if (topScrollbarRef.current)
topScrollbarRef.current.scrollLeft = scrollLeft;
};

// Swap sort direction.
const getSortDirection = (prevSortDir: 'ascending' | 'descending') => {
if (prevSortDir === 'descending') {
Expand All @@ -53,102 +63,131 @@ export const Table = ({
}
};

const [width, setWidth] = useState(0);

return (
<div
id={`${id}-container`}
className={classNames(
{ 'usa-table-container': !scrollable },
{ 'usa-table-container--scrollable': scrollable },
)}
ref={(node) => {
if (node && sortable) {
table.on(node);
}
}}
>
<table
<div className={className}>
<div
className="scroll-container"
onScroll={handleScroll}
ref={topScrollbarRef}
>
<div
className="scrollbar"
id={`${id}-top-scrollbar`}
style={{
width,
}}
></div>
</div>
<div
id={`${id}-container`}
className={classNames(
'usa-table',
{ 'usa-table--borderless': borderless },
{ 'usa-table--striped': striped },
{ 'usa-table--stacked': stacked },
{ 'usa-table--sticky-header': stickyHeader },
'layout-fixed',
'width-full',
'whitespace-wrap',
className,
{ 'usa-table-container': !scrollable },
{ 'usa-table-container--scrollable': scrollable },
'margin-top-0',
'scroll-container',
)}
tabIndex={scrollable ? Math.max(0, tabIndex) : tabIndex}
onScroll={handleScroll}
ref={(node) => {
contentRef.current = node;
if (node && sortable) {
table.on(node);
}
}}
>
<caption hidden={!caption} className="text-italic">
{caption}
</caption>
<thead>
<tr>
{columns
.map((obj) => ({
...obj,
sortable: obj.sortable !== undefined ? obj.sortable : true,
}))
.map((column: TableColumn, index: number) => (
<th
id={`${id}-${column.id}`}
key={column.id}
data-sortable={(sortable && column.sortable) || null}
scope="col"
role="columnheader"
aria-sort={
sortable && column.sortable && sortIndex === index
? sortDir
: undefined
}
onClick={() => handleHeaderClick(index)}
style={{ width: column.width ? `${column.width}px` : 'auto' }}
>
{column.name}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i: number) => {
const rowData: TableCell[] = [];
row.forEach((cell: string | number | TableCell) => {
if (sortable) {
rowData.push({
value: isCellSpec(cell) ? cell.value : cell,
sortValue: isCellSpec(cell)
? (cell.sortValue ?? cell.value ?? '').toString()
: cell,
});
} else {
rowData.push({
value: isCellSpec(cell) ? cell.value : cell,
});
}
});

return (
<tr key={`tr-${i}`}>
{rowData.map((col, j) => (
<td
key={`td-${j}`}
data-sort-value={sortable ? col.sortValue : col.value}
<table
className={classNames(
'usa-table',
{ 'usa-table--borderless': borderless },
{ 'usa-table--striped': striped },
{ 'usa-table--stacked': stacked },
{ 'usa-table--sticky-header': stickyHeader },
'layout-fixed',
'width-full',
'whitespace-wrap',
)}
ref={(node) => {
if (!node) {
setWidth(0);
return;
}
setWidth(node.offsetWidth);
}}
tabIndex={scrollable ? Math.max(0, tabIndex) : tabIndex}
>
<caption hidden={!caption} className="text-italic">
{caption}
</caption>
<thead>
<tr>
{columns
.map((obj) => ({
...obj,
sortable: obj.sortable !== undefined ? obj.sortable : true,
}))
.map((column: TableColumn, index: number) => (
<th
id={`${id}-${column.id}`}
key={column.id}
data-sortable={(sortable && column.sortable) || null}
scope="col"
role="columnheader"
aria-sort={
sortable && column.sortable && sortIndex === index
? sortDir
: undefined
}
onClick={() => handleHeaderClick(index)}
style={{
width: column.width ? `${column.width}px` : 'auto',
}}
>
{col.value}
</td>
{column.name}
</th>
))}
</tr>
);
})}
</tbody>
</table>
{sortable && (
<div
className="usa-sr-only usa-table__announcement-region"
aria-live="polite"
></div>
)}
</tr>
</thead>
<tbody>
{data.map((row, i: number) => {
const rowData: TableCell[] = [];
row.forEach((cell: string | number | TableCell) => {
if (sortable) {
rowData.push({
value: isCellSpec(cell) ? cell.value : cell,
sortValue: isCellSpec(cell)
? (cell.sortValue ?? cell.value ?? '').toString()
: cell,
});
} else {
rowData.push({
value: isCellSpec(cell) ? cell.value : cell,
});
}
});

return (
<tr key={`tr-${i}`}>
{rowData.map((col, j) => (
<td
key={`td-${j}`}
data-sort-value={sortable ? col.sortValue : col.value}
>
{col.value}
</td>
))}
</tr>
);
})}
</tbody>
</table>
{sortable && (
<div
className="usa-sr-only usa-table__announcement-region"
aria-live="polite"
></div>
)}
</div>
</div>
);
};
Expand Down
39 changes: 29 additions & 10 deletions app/client/src/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,10 @@ function FilterFieldInputs({
? sourceState[sourceFieldConfig.id]
: null;
const selectProps = {
additionalOptions: 'additionalOptions' in fieldConfig ? fieldConfig.additionalOptions : [],
additionalOptions:
'additionalOptions' in fieldConfig
? fieldConfig.additionalOptions
: [],
apiKey,
apiUrl,
contextFilters: getContextFilters(
Expand Down Expand Up @@ -709,13 +712,13 @@ function FilterFieldInputs({
<div className="grid-gap-2 grid-row">
{fieldsJsx.map(([field, key, size]) => (
<div
className={`flex-align-self-end ${
className={
size === 'large'
? 'width-full'
: size === 'medium'
? 'desktop:grid-col-8 tablet:grid-col-6'
: 'desktop:grid-col-4 tablet:grid-col-6'
}`}
}
key={key}
>
{field}
Expand Down Expand Up @@ -1501,11 +1504,21 @@ function filterDynamicOptions({
const label = secondaryValue || value;
return { label, value };
});
if (!lastLoadedOption) {
(additionalOptions ?? [])
.concat(defaultOption ? [defaultOption] : [])
.forEach((option) => {
if (
!inputValue ||
(typeof option.label === 'string' &&
option.label.toLowerCase().includes(inputValue.toLowerCase()))
) {
options.unshift(option);
}
});
}
return {
options:
!lastLoadedOption && defaultOption // only include default option in first page
? [defaultOption, ...additionalOptions, ...options]
: options,
options,
hasMore: options.length >= limit,
};
};
Expand Down Expand Up @@ -1624,8 +1637,10 @@ function getContextFilters(

// Returns the default state for inputs
function getDefaultFilterState(filterFields: FilterFields) {
const hasParams =
Array.from(new URLSearchParams(window.location.search)).length > 0;
return Object.values(filterFields).reduce((a, b) => {
const defaultValue = getDefaultValue(b);
const defaultValue = getDefaultValue(b, !hasParams);
const defaultState =
defaultValue && isMultiOptionField(b) ? [defaultValue] : defaultValue;
return { ...a, [b.key]: defaultState };
Expand All @@ -1641,8 +1656,12 @@ function getDefaultSourceState(sourceFields: SourceFields) {
}, {}) as SourceFieldState;
}

function getDefaultValue(field: FilterField | SourceField) {
const defaultValue = 'default' in field ? field.default : null;
function getDefaultValue(
field: FilterField | SourceField,
useConfiguredDefault = true,
) {
const defaultValue =
useConfiguredDefault && 'default' in field ? field.default : null;
return defaultValue ?? (isSingleValueField(field) ? '' : null);
}

Expand Down
Loading
Loading