Support row click
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -4,6 +4,8 @@ import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { TableSnippet } from '@/snippets/stories-snippets';
|
||||
import {
|
||||
tablePropDefs,
|
||||
tableHeaderPropDefs,
|
||||
tableBodyPropDefs,
|
||||
tablePaginationPropDefs,
|
||||
tableUsageSnippet,
|
||||
tableBasicSnippet,
|
||||
@@ -13,6 +15,9 @@ import {
|
||||
tablePaginationSnippet,
|
||||
tableSelectionSnippet,
|
||||
tableSortingSnippet,
|
||||
columnPropDefs,
|
||||
rowPropDefs,
|
||||
cellPropDefs,
|
||||
} from './table.props';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
@@ -39,13 +44,43 @@ import { Theming } from '@/components/Theming';
|
||||
|
||||
The main table component that renders data in a structured grid format.
|
||||
|
||||
Coming soon.
|
||||
<PropsTable data={tablePropDefs} />
|
||||
|
||||
### TableHeader
|
||||
|
||||
The header section of the table that contains the column definitions.
|
||||
|
||||
<PropsTable data={tableHeaderPropDefs} />
|
||||
|
||||
### Column
|
||||
|
||||
A column definition that describes how a column should be rendered.
|
||||
|
||||
<PropsTable data={columnPropDefs} />
|
||||
|
||||
### TableBody
|
||||
|
||||
The body section of the table that contains the rows.
|
||||
|
||||
<PropsTable data={tableBodyPropDefs} />
|
||||
|
||||
### Row
|
||||
|
||||
A row definition that describes how a row should be rendered.
|
||||
|
||||
<PropsTable data={rowPropDefs} />
|
||||
|
||||
### Cell
|
||||
|
||||
A cell definition that describes how a cell should be rendered.
|
||||
|
||||
<PropsTable data={cellPropDefs} />
|
||||
|
||||
### TablePagination
|
||||
|
||||
A pagination component designed to work with the Table component.
|
||||
|
||||
Coming soon.
|
||||
<PropsTable data={tablePaginationPropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -53,6 +88,10 @@ Coming soon.
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Row selection
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Row Clicks
|
||||
|
||||
Coming soon.
|
||||
@@ -65,6 +104,30 @@ Coming soon.
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Asynchronous loading
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Empty state
|
||||
|
||||
Coming soon.
|
||||
|
||||
### Column resizing
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
|
||||
### Column reordering
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
|
||||
### Column pinning
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
|
||||
### Column visibility
|
||||
|
||||
This feature is not available yet — let us know if you'd like us to explore it!
|
||||
|
||||
<Theming component="Table" />
|
||||
|
||||
<ChangelogComponent component="table" />
|
||||
|
||||
@@ -5,72 +5,264 @@ import {
|
||||
} from '../../utils/propDefs';
|
||||
|
||||
export const tablePropDefs: Record<string, PropDef> = {
|
||||
table: {
|
||||
type: 'complex',
|
||||
complexType: {
|
||||
name: 'Table<TData>',
|
||||
properties: {
|
||||
'TanStack Table instance': {
|
||||
type: 'object',
|
||||
required: true,
|
||||
description:
|
||||
'The TanStack Table instance created with useReactTable()',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
selectionBehavior: {
|
||||
type: 'enum',
|
||||
values: ['toggle', 'replace'],
|
||||
default: 'toggle',
|
||||
description: 'How multiple selection should behave in the collection.',
|
||||
},
|
||||
onRowClick: {
|
||||
type: 'complex',
|
||||
complexType: {
|
||||
name: '(row: TData, event: React.MouseEvent<HTMLTableRowElement>) => void',
|
||||
properties: {
|
||||
row: {
|
||||
type: 'TData',
|
||||
required: true,
|
||||
description: 'The row data object',
|
||||
},
|
||||
event: {
|
||||
type: 'React.MouseEvent<HTMLTableRowElement>',
|
||||
required: true,
|
||||
description: 'The mouse event object',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
disabledBehavior: {
|
||||
type: 'enum',
|
||||
values: ['selection', 'all'],
|
||||
default: 'selection',
|
||||
description:
|
||||
'Whether disabledKeys applies to all interactions, or only selection.',
|
||||
},
|
||||
disabledKeys: {
|
||||
type: 'enum',
|
||||
values: ['Iterable<Key>'],
|
||||
description: 'A list of row keys to disable.',
|
||||
},
|
||||
selectionMode: {
|
||||
type: 'enum',
|
||||
values: ['single', 'multiple'],
|
||||
description: 'The type of selection that is allowed in the collection.',
|
||||
},
|
||||
selectedKeys: {
|
||||
type: 'enum',
|
||||
values: ['all', 'Iterable<Key>'],
|
||||
description: 'The currently selected keys in the collection (controlled).',
|
||||
},
|
||||
defaultSelectedKeys: {
|
||||
type: 'enum',
|
||||
values: ['all', 'Iterable<Key>'],
|
||||
description: 'The initial selected keys in the collection (uncontrolled).',
|
||||
},
|
||||
onRowAction: {
|
||||
type: 'enum',
|
||||
values: ['(key: Key) => void'],
|
||||
description:
|
||||
'Handler that is called when a user performs an action on the row.',
|
||||
},
|
||||
onSelectionChange: {
|
||||
type: 'enum',
|
||||
values: ['(keys: Selection) => void'],
|
||||
description: 'Handler that is called when the selection changes.',
|
||||
},
|
||||
onSortChange: {
|
||||
type: 'enum',
|
||||
values: ['(descriptor: SortDescriptor) => any'],
|
||||
description:
|
||||
'Handler that is called when the sorted column or direction changes.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tableHeaderPropDefs: Record<string, PropDef> = {
|
||||
onHoverStart: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction starts.',
|
||||
},
|
||||
onHoverEnd: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction ends.',
|
||||
},
|
||||
onHoverChange: {
|
||||
type: 'enum',
|
||||
values: ['(isHovering: boolean) => void'],
|
||||
description: 'Handler that is called when the hover state changes.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const columnPropDefs: Record<string, PropDef> = {
|
||||
id: {
|
||||
type: 'enum',
|
||||
values: ['Key'],
|
||||
description: 'The unique id of the column.',
|
||||
},
|
||||
allowsSorting: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the column allows sorting.',
|
||||
},
|
||||
isRowHeader: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether a column is a row header and should be announced by assistive technology during row navigation.',
|
||||
},
|
||||
textValue: {
|
||||
type: 'string',
|
||||
description:
|
||||
"A string representation of the column's contents, used for accessibility announcements.",
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tableBodyPropDefs: Record<string, PropDef> = {
|
||||
renderEmptyState: {
|
||||
type: 'enum',
|
||||
values: ['(props) => ReactNode'],
|
||||
description:
|
||||
'Provides content to display when there are no rows in the table.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const rowPropDefs: Record<string, PropDef> = {
|
||||
textValue: {
|
||||
type: 'string',
|
||||
description:
|
||||
"A string representation of the row's contents, used for accessibility announcements.",
|
||||
},
|
||||
isDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the row is disabled.',
|
||||
},
|
||||
id: {
|
||||
type: 'enum',
|
||||
values: ['Key'],
|
||||
description: 'The unique id of the row.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
description: 'The URL to navigate to when the row is clicked.',
|
||||
},
|
||||
hrefLang: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The language of the URL to navigate to when the row is clicked.',
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The target of the URL to navigate to when the row is clicked.',
|
||||
},
|
||||
rel: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The relationship of the URL to navigate to when the row is clicked.',
|
||||
},
|
||||
onAction: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description:
|
||||
"Handler that is called when a user performs an action on the row. The exact user event depends on the collection's selectionBehavior prop and the interaction modality.",
|
||||
},
|
||||
onHoverStart: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction starts.',
|
||||
},
|
||||
onHoverEnd: {
|
||||
type: 'enum',
|
||||
values: ['(e: HoverEvent) => void'],
|
||||
description: 'Handler that is called when a hover interaction ends.',
|
||||
},
|
||||
onHoverChange: {
|
||||
type: 'enum',
|
||||
values: ['(isHovering: boolean) => void'],
|
||||
description: 'Handler that is called when the hover state changes.',
|
||||
},
|
||||
onPress: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description:
|
||||
'Handler that is called when the press is released over the target.',
|
||||
},
|
||||
onPressStart: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description: 'Handler that is called when a press interaction starts.',
|
||||
},
|
||||
onPressEnd: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description:
|
||||
'Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target.',
|
||||
},
|
||||
onPressChange: {
|
||||
type: 'enum',
|
||||
values: ['(isPressed: boolean) => void'],
|
||||
description: 'Handler that is called when the press state changes.',
|
||||
},
|
||||
onPressUp: {
|
||||
type: 'enum',
|
||||
values: ['(e: PressEvent) => void'],
|
||||
description:
|
||||
'Handler that is called when a press is released over the target, regardless of whether it started on the target or not.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const cellPropDefs: Record<string, PropDef> = {
|
||||
id: {
|
||||
type: 'enum',
|
||||
values: ['Key'],
|
||||
description: 'The unique id of the cell.',
|
||||
},
|
||||
textValue: {
|
||||
type: 'string',
|
||||
description:
|
||||
"A string representation of the cell's contents, used for features like typeahead.",
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tablePaginationPropDefs: Record<string, PropDef> = {
|
||||
table: {
|
||||
type: 'complex',
|
||||
complexType: {
|
||||
name: 'Table<TData>',
|
||||
properties: {
|
||||
'TanStack Table instance': {
|
||||
type: 'object',
|
||||
required: true,
|
||||
description:
|
||||
'The TanStack Table instance (same instance passed to Table component)',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
pageIndex: {
|
||||
type: 'number',
|
||||
description: 'The current page index.',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
description: 'The number of items per page.',
|
||||
},
|
||||
setPageIndex: {
|
||||
type: 'enum',
|
||||
values: ['(pageIndex: number) => void'],
|
||||
description: 'Handler that is called when the page index changes.',
|
||||
},
|
||||
setPageSize: {
|
||||
type: 'enum',
|
||||
values: ['(pageSize: number) => void'],
|
||||
description: 'Handler that is called when the page size changes.',
|
||||
},
|
||||
rowCount: {
|
||||
type: 'number',
|
||||
description: 'The total number of rows in the table.',
|
||||
},
|
||||
onNextPage: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description: 'Handler that is called when the next page is requested.',
|
||||
},
|
||||
onPreviousPage: {
|
||||
type: 'enum',
|
||||
values: ['() => void'],
|
||||
description: 'Handler that is called when the previous page is requested.',
|
||||
},
|
||||
onPageSizeChange: {
|
||||
type: 'enum',
|
||||
values: ['(pageSize: number) => void'],
|
||||
description: 'Handler that is called when the page size changes.',
|
||||
},
|
||||
showPageSizeOptions: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to show the page size options.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tableUsageSnippet = `import {
|
||||
Cell,
|
||||
CellProfile,
|
||||
Column,
|
||||
Row,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TablePagination,
|
||||
} from '@backstage/ui';
|
||||
export const tableUsageSnippet = `import { Cell, ..., TableHeader, TablePagination } from '@backstage/ui';
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
||||
@@ -28,6 +28,7 @@ export type PropDef = {
|
||||
default?: string;
|
||||
required?: boolean;
|
||||
responsive?: boolean;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export { breakpoints };
|
||||
|
||||
@@ -9702,19 +9702,6 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bui-Hidden {
|
||||
clip: rect(0 0 0 0) !important;
|
||||
clip-path: inset(100%) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
margin: -1px !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.bui-Icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@@ -10069,6 +10056,10 @@
|
||||
.bui-TableRow {
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
transition: color .2s ease-in-out;
|
||||
|
||||
&[data-react-aria-pressable="true"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-TableBody .bui-TableRow:hover {
|
||||
|
||||
@@ -1050,9 +1050,6 @@ export const heightPropDefs: {
|
||||
// @public (undocumented)
|
||||
export type HeightProps = GetPropDefTypes<typeof heightPropDefs>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const Hidden: (props: BoxProps) => JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const Icon: (props: IconProps) => JSX_2.Element | null;
|
||||
|
||||
@@ -1413,6 +1410,7 @@ export function Row<T extends object>({
|
||||
id,
|
||||
columns,
|
||||
children,
|
||||
href,
|
||||
...otherProps
|
||||
}: RowProps<T>): JSX_2.Element;
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Hidden } from './Hidden';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Hidden',
|
||||
component: Hidden,
|
||||
} satisfies Meta<typeof Hidden>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Visually hidden content',
|
||||
},
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.bui-Hidden {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important; /* move it off-screen */
|
||||
overflow: hidden !important;
|
||||
clip: rect(0 0 0 0) !important; /* legacy clip for IE/Edge */
|
||||
clip-path: inset(100%) !important; /* modern clip */
|
||||
white-space: nowrap !important; /* avoid line breaks */
|
||||
border: 0 !important;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Box, BoxProps } from '../Box';
|
||||
import clsx from 'clsx';
|
||||
|
||||
/** @public */
|
||||
export const Hidden = (props: BoxProps) => {
|
||||
return <Box {...props} className={clsx('bui-Hidden', props.className)} />;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Hidden } from './Hidden';
|
||||
@@ -20,28 +20,7 @@ import clsx from 'clsx';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import type { LinkProps } from './types';
|
||||
import { useNavigate, useHref } from 'react-router-dom';
|
||||
|
||||
// Helper function to determine if a link is external
|
||||
function isExternalLink(href?: string): boolean {
|
||||
if (!href) return false;
|
||||
|
||||
// Check if it's an absolute URL with protocol
|
||||
if (href.startsWith('http://') || href.startsWith('https://')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a protocol-relative URL
|
||||
if (href.startsWith('//')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a mailto: or tel: link
|
||||
if (href.startsWith('mailto:') || href.startsWith('tel:')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
import { isExternalLink } from '../../utils/isExternalLink';
|
||||
|
||||
/** @public */
|
||||
export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
|
||||
|
||||
@@ -172,6 +172,98 @@ export const TableRockBand: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const RowClick: Story = {
|
||||
render: () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
|
||||
const newData = data4.slice(
|
||||
pageIndex * pageSize,
|
||||
(pageIndex + 1) * pageSize,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<Column isRowHeader>Band name</Column>
|
||||
<Column>Genre</Column>
|
||||
<Column>Year formed</Column>
|
||||
<Column>Albums</Column>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{newData.map(item => (
|
||||
<Row key={item.name} onAction={() => alert('Row clicked')}>
|
||||
<CellProfileBUI
|
||||
name={item.name}
|
||||
src={item.image}
|
||||
href={item.website}
|
||||
/>
|
||||
<Cell title={item.genre} />
|
||||
<Cell title={item.yearFormed.toString()} />
|
||||
<Cell title={item.albums.toString()} />
|
||||
</Row>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
rowCount={data4.length}
|
||||
setPageIndex={setPageIndex}
|
||||
setPageSize={setPageSize}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const RowLink: Story = {
|
||||
render: () => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
|
||||
const newData = data4.slice(
|
||||
pageIndex * pageSize,
|
||||
(pageIndex + 1) * pageSize,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<Column isRowHeader>Band name</Column>
|
||||
<Column>Genre</Column>
|
||||
<Column>Year formed</Column>
|
||||
<Column>Albums</Column>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{newData.map(item => (
|
||||
<Row key={item.name} href="/band">
|
||||
<CellProfileBUI
|
||||
name={item.name}
|
||||
src={item.image}
|
||||
href={item.website}
|
||||
/>
|
||||
<Cell title={item.genre} />
|
||||
<Cell title={item.yearFormed.toString()} />
|
||||
<Cell title={item.albums.toString()} />
|
||||
</Row>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
rowCount={data4.length}
|
||||
setPageIndex={setPageIndex}
|
||||
setPageSize={setPageSize}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CellText: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
|
||||
@@ -66,6 +66,10 @@
|
||||
.bui-TableRow {
|
||||
border-bottom: 1px solid var(--bui-border);
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
&[data-react-aria-pressable='true'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-TableBody .bui-TableRow:hover {
|
||||
|
||||
@@ -21,28 +21,56 @@ import {
|
||||
Cell,
|
||||
Collection,
|
||||
Checkbox,
|
||||
RouterProvider,
|
||||
} from 'react-aria-components';
|
||||
import { useStyles } from '../../../hooks/useStyles';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useHref } from 'react-router-dom';
|
||||
import { isExternalLink } from '../../../utils/isExternalLink';
|
||||
|
||||
/** @public */
|
||||
export function Row<T extends object>({
|
||||
id,
|
||||
columns,
|
||||
children,
|
||||
href,
|
||||
...otherProps
|
||||
}: RowProps<T>) {
|
||||
const { classNames } = useStyles('Table');
|
||||
const navigate = useNavigate();
|
||||
const isExternal = isExternalLink(href);
|
||||
|
||||
let { selectionBehavior } = useTableOptions();
|
||||
|
||||
return (
|
||||
<ReactAriaRow id={id} className={classNames.row} {...otherProps}>
|
||||
const content = (
|
||||
<>
|
||||
{selectionBehavior === 'toggle' && (
|
||||
<Cell>
|
||||
<Checkbox slot="selection" />
|
||||
</Cell>
|
||||
)}
|
||||
<Collection items={columns}>{children}</Collection>
|
||||
</ReactAriaRow>
|
||||
</>
|
||||
);
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<ReactAriaRow id={id} className={classNames.row} {...otherProps}>
|
||||
{content}
|
||||
</ReactAriaRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
<ReactAriaRow
|
||||
id={id}
|
||||
className={classNames.row}
|
||||
data-react-aria-pressable="true"
|
||||
{...otherProps}
|
||||
>
|
||||
{content}
|
||||
</ReactAriaRow>
|
||||
</RouterProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
@import '../components/Grid/styles.css';
|
||||
@import '../components/Header/Header.styles.css';
|
||||
@import '../components/HeaderPage/HeaderPage.styles.css';
|
||||
@import '../components/Hidden/Hidden.styles.css';
|
||||
@import '../components/Icon/styles.css';
|
||||
@import '../components/Link/styles.css';
|
||||
@import '../components/Menu/Menu.styles.css';
|
||||
|
||||
@@ -37,7 +37,6 @@ export * from './components/Collapsible';
|
||||
export * from './components/FieldLabel';
|
||||
export * from './components/Header';
|
||||
export * from './components/HeaderPage';
|
||||
export * from './components/Hidden';
|
||||
export * from './components/Icon';
|
||||
export * from './components/ButtonIcon';
|
||||
export * from './components/ButtonLink';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines if a link is external.
|
||||
* @param href - The href of the link.
|
||||
* @returns True if the link is external, false otherwise.
|
||||
* @internal
|
||||
*/
|
||||
export function isExternalLink(href?: string): boolean {
|
||||
if (!href) return false;
|
||||
|
||||
// Check if it's an absolute URL with protocol
|
||||
if (href.startsWith('http://') || href.startsWith('https://')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a protocol-relative URL
|
||||
if (href.startsWith('//')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a mailto: or tel: link
|
||||
if (href.startsWith('mailto:') || href.startsWith('tel:')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user