@patternfly/react-docs 7.6.0-prerelease.7 → 7.6.0-prerelease.8
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 +12 -13
- package/patternfly-docs/generated/components/about-modal/react.js +149 -0
- package/patternfly-docs/generated/components/accordion/react.js +262 -0
- package/patternfly-docs/generated/components/action-list/react.js +144 -0
- package/patternfly-docs/generated/components/alert/react-demos.js +56 -0
- package/patternfly-docs/generated/components/alert/react.js +1433 -0
- package/patternfly-docs/generated/components/avatar/react.js +166 -0
- package/patternfly-docs/generated/components/back-to-top/react-demos.js +60 -0
- package/patternfly-docs/generated/components/back-to-top/react.js +77 -0
- package/patternfly-docs/generated/components/backdrop/react.js +64 -0
- package/patternfly-docs/generated/components/background-image/react.js +62 -0
- package/patternfly-docs/generated/components/badge/react.js +97 -0
- package/patternfly-docs/generated/components/banner/react-demos.js +57 -0
- package/patternfly-docs/generated/components/banner/react.js +148 -0
- package/patternfly-docs/generated/components/brand/react.js +142 -0
- package/patternfly-docs/generated/components/breadcrumb/react.js +206 -0
- package/patternfly-docs/generated/components/button/react-demos.js +57 -0
- package/patternfly-docs/generated/components/button/react.js +826 -0
- package/patternfly-docs/generated/components/card/react-demos.js +201 -0
- package/patternfly-docs/generated/components/card/react.js +1015 -0
- package/patternfly-docs/generated/components/charts/area-chart/-Victory.js +1350 -0
- package/patternfly-docs/generated/components/charts/bar-chart/-Victory.js +1334 -0
- package/patternfly-docs/generated/components/charts/box-plot-chart/-Victory.js +1282 -0
- package/patternfly-docs/generated/components/charts/bullet-chart/-Victory.js +848 -0
- package/patternfly-docs/generated/components/charts/colors-for-charts/-Victory.js +192 -0
- package/patternfly-docs/generated/components/charts/donut-chart/-Victory.js +426 -0
- package/patternfly-docs/generated/components/charts/donut-utilization-chart/-Victory.js +804 -0
- package/patternfly-docs/generated/components/charts/legends/-Victory.js +3230 -0
- package/patternfly-docs/generated/components/charts/line-chart/-Victory.js +1178 -0
- package/patternfly-docs/generated/components/charts/line-chart/ECharts.js +525 -0
- package/patternfly-docs/generated/components/charts/patterns/-Victory.js +3382 -0
- package/patternfly-docs/generated/components/charts/pie-chart/-Victory.js +377 -0
- package/patternfly-docs/generated/components/charts/resize-observer/-Victory.js +2475 -0
- package/patternfly-docs/generated/components/charts/sankey-chart/ECharts.js +538 -0
- package/patternfly-docs/generated/components/charts/scatter-chart/-Victory.js +1551 -0
- package/patternfly-docs/generated/components/charts/skeletons/-Victory.js +4115 -0
- package/patternfly-docs/generated/components/charts/sparkline-chart/-Victory.js +955 -0
- package/patternfly-docs/generated/components/charts/stack-chart/-Victory.js +1173 -0
- package/patternfly-docs/generated/components/charts/threshold-chart/-Victory.js +1166 -0
- package/patternfly-docs/generated/components/charts/tooltips/-Victory.js +413 -0
- package/patternfly-docs/generated/components/chip/react-deprecated.js +323 -0
- package/patternfly-docs/generated/components/clipboard-copy/react.js +373 -0
- package/patternfly-docs/generated/components/code-block/react.js +148 -0
- package/patternfly-docs/generated/components/code-editor/react.js +659 -0
- package/patternfly-docs/generated/components/compass/react-demos.js +147 -0
- package/patternfly-docs/generated/components/compass/react.js +440 -0
- package/patternfly-docs/generated/components/content/react.js +248 -0
- package/patternfly-docs/generated/components/data-list/react-demos.js +90 -0
- package/patternfly-docs/generated/components/data-list/react.js +709 -0
- package/patternfly-docs/generated/components/date-and-time/calendar-month/react.js +283 -0
- package/patternfly-docs/generated/components/date-and-time/date-and-time-picker/react-demos.js +64 -0
- package/patternfly-docs/generated/components/date-and-time/date-picker/react-demos.js +83 -0
- package/patternfly-docs/generated/components/date-and-time/date-picker/react.js +395 -0
- package/patternfly-docs/generated/components/date-and-time/time-picker/react.js +241 -0
- package/patternfly-docs/generated/components/description-list/react-demos.js +58 -0
- package/patternfly-docs/generated/components/description-list/react.js +743 -0
- package/patternfly-docs/generated/components/divider/react.js +126 -0
- package/patternfly-docs/generated/components/drag-and-drop/react-demos.js +351 -0
- package/patternfly-docs/generated/components/drag-and-drop/react-deprecated.js +184 -0
- package/patternfly-docs/generated/components/drag-and-drop/react.js +137 -0
- package/patternfly-docs/generated/components/drawer/react.js +598 -0
- package/patternfly-docs/generated/components/dual-list-selector/react-deprecated.js +772 -0
- package/patternfly-docs/generated/components/dual-list-selector/react.js +594 -0
- package/patternfly-docs/generated/components/empty-state/react.js +199 -0
- package/patternfly-docs/generated/components/expandable-section/react-demos.js +65 -0
- package/patternfly-docs/generated/components/expandable-section/react.js +408 -0
- package/patternfly-docs/generated/components/file-upload/multiple-file-upload/react-demos.js +52 -0
- package/patternfly-docs/generated/components/file-upload/multiple-file-upload/react.js +398 -0
- package/patternfly-docs/generated/components/file-upload/simple-file-upload/react.js +749 -0
- package/patternfly-docs/generated/components/forms/checkbox/react.js +222 -0
- package/patternfly-docs/generated/components/forms/form/react.js +1106 -0
- package/patternfly-docs/generated/components/forms/form-select/react.js +208 -0
- package/patternfly-docs/generated/components/forms/radio/react.js +212 -0
- package/patternfly-docs/generated/components/forms/text-area/react.js +160 -0
- package/patternfly-docs/generated/components/forms/text-input/react.js +216 -0
- package/patternfly-docs/generated/components/helper-text/react-demos.js +180 -0
- package/patternfly-docs/generated/components/helper-text/react.js +164 -0
- package/patternfly-docs/generated/components/hero/react.js +88 -0
- package/patternfly-docs/generated/components/hint/react.js +169 -0
- package/patternfly-docs/generated/components/icon/react.js +215 -0
- package/patternfly-docs/generated/components/input-group/react.js +182 -0
- package/patternfly-docs/generated/components/jump-links/react-demos.js +154 -0
- package/patternfly-docs/generated/components/jump-links/react.js +212 -0
- package/patternfly-docs/generated/components/label/react-demos.js +57 -0
- package/patternfly-docs/generated/components/label/react.js +417 -0
- package/patternfly-docs/generated/components/list/react.js +175 -0
- package/patternfly-docs/generated/components/login-page/react.js +587 -0
- package/patternfly-docs/generated/components/masthead/react-demos.js +79 -0
- package/patternfly-docs/generated/components/masthead/react.js +291 -0
- package/patternfly-docs/generated/components/menus/application-launcher/react-demos.js +769 -0
- package/patternfly-docs/generated/components/menus/context-selector/react-demos.js +665 -0
- package/patternfly-docs/generated/components/menus/custom-menus/react-demos.js +187 -0
- package/patternfly-docs/generated/components/menus/dropdown/react-templates.js +163 -0
- package/patternfly-docs/generated/components/menus/dropdown/react.js +998 -0
- package/patternfly-docs/generated/components/menus/menu/react.js +1540 -0
- package/patternfly-docs/generated/components/menus/menu-toggle/react.js +747 -0
- package/patternfly-docs/generated/components/menus/options-menu/react-demos.js +508 -0
- package/patternfly-docs/generated/components/menus/select/react-templates.js +257 -0
- package/patternfly-docs/generated/components/menus/select/react.js +998 -0
- package/patternfly-docs/generated/components/modal/react-deprecated.js +554 -0
- package/patternfly-docs/generated/components/modal/react.js +597 -0
- package/patternfly-docs/generated/components/navigation/react-demos.js +356 -0
- package/patternfly-docs/generated/components/navigation/react.js +409 -0
- package/patternfly-docs/generated/components/notification-badge/react.js +196 -0
- package/patternfly-docs/generated/components/notification-drawer/react-demos.js +107 -0
- package/patternfly-docs/generated/components/notification-drawer/react.js +394 -0
- package/patternfly-docs/generated/components/number-input/react.js +210 -0
- package/patternfly-docs/generated/components/overflow-menu/react.js +274 -0
- package/patternfly-docs/generated/components/page/react-demos.js +149 -0
- package/patternfly-docs/generated/components/page/react.js +1352 -0
- package/patternfly-docs/generated/components/pagination/react.js +492 -0
- package/patternfly-docs/generated/components/panel/react.js +236 -0
- package/patternfly-docs/generated/components/popover/react.js +390 -0
- package/patternfly-docs/generated/components/progress/react-demos.js +59 -0
- package/patternfly-docs/generated/components/progress/react.js +283 -0
- package/patternfly-docs/generated/components/progress-stepper/react-demos.js +45 -0
- package/patternfly-docs/generated/components/progress-stepper/react.js +219 -0
- package/patternfly-docs/generated/components/search-input/react-demos.js +113 -0
- package/patternfly-docs/generated/components/search-input/react.js +263 -0
- package/patternfly-docs/generated/components/sidebar/react.js +236 -0
- package/patternfly-docs/generated/components/simple-list/react.js +200 -0
- package/patternfly-docs/generated/components/skeleton/react-demos.js +44 -0
- package/patternfly-docs/generated/components/skeleton/react.js +122 -0
- package/patternfly-docs/generated/components/skip-to-content/react.js +73 -0
- package/patternfly-docs/generated/components/slider/react.js +309 -0
- package/patternfly-docs/generated/components/spinner/react.js +111 -0
- package/patternfly-docs/generated/components/switch/react.js +163 -0
- package/patternfly-docs/generated/components/table/react-demos.js +355 -0
- package/patternfly-docs/generated/components/table/react-deprecated.js +1350 -0
- package/patternfly-docs/generated/components/table/react.js +3241 -0
- package/patternfly-docs/generated/components/tabs/react-demos.js +108 -0
- package/patternfly-docs/generated/components/tabs/react.js +1359 -0
- package/patternfly-docs/generated/components/text-input-group/react-demos.js +152 -0
- package/patternfly-docs/generated/components/text-input-group/react.js +278 -0
- package/patternfly-docs/generated/components/tile/react-deprecated.js +242 -0
- package/patternfly-docs/generated/components/timestamp/react.js +283 -0
- package/patternfly-docs/generated/components/title/react.js +94 -0
- package/patternfly-docs/generated/components/toggle-group/react.js +299 -0
- package/patternfly-docs/generated/components/toolbar/react-demos.js +66 -0
- package/patternfly-docs/generated/components/toolbar/react.js +932 -0
- package/patternfly-docs/generated/components/tooltip/react.js +241 -0
- package/patternfly-docs/generated/components/tree-view/react.js +429 -0
- package/patternfly-docs/generated/components/truncate/react.js +211 -0
- package/patternfly-docs/generated/components/wizard/react-demos.js +87 -0
- package/patternfly-docs/generated/components/wizard/react-deprecated.js +788 -0
- package/patternfly-docs/generated/components/wizard/react.js +986 -0
- package/patternfly-docs/generated/developer-guides/open-ui-automation/react.js +285 -0
- package/patternfly-docs/generated/foundations-and-styles/layouts/bullseye/react.js +70 -0
- package/patternfly-docs/generated/foundations-and-styles/layouts/flex/react.js +506 -0
- package/patternfly-docs/generated/foundations-and-styles/layouts/gallery/react.js +94 -0
- package/patternfly-docs/generated/foundations-and-styles/layouts/grid/react.js +272 -0
- package/patternfly-docs/generated/foundations-and-styles/layouts/level/react.js +87 -0
- package/patternfly-docs/generated/foundations-and-styles/layouts/split/react.js +124 -0
- package/patternfly-docs/generated/foundations-and-styles/layouts/stack/react.js +112 -0
- package/patternfly-docs/generated/index.js +1769 -0
- package/patternfly-docs/generated/patterns/card-view/react-demos.js +78 -0
- package/patternfly-docs/generated/patterns/filters/react-demos.js +141 -0
- package/patternfly-docs/generated/patterns/password-generator/react-demos.js +51 -0
- package/patternfly-docs/generated/patterns/password-strength/react-demos.js +61 -0
- package/patternfly-docs/generated/patterns/primary-detail/react-demos.js +124 -0
- package/patternfly-docs/generated/patterns/right-to-left/react-demos.js +81 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AutoLinkHeader, Example, Link as PatternflyThemeLink } from '@patternfly/documentation-framework/components';
|
|
3
|
+
import { Fragment, useState } from 'react';
|
|
4
|
+
import TrashIcon from '@patternfly/react-icons/dist/esm/icons/trash-icon';
|
|
5
|
+
import RhUiAddCircleFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-circle-fill-icon';
|
|
6
|
+
import pfIcon from '../../../../../react-core/src/demos/CardView/../assets/pf-logo-small.svg';
|
|
7
|
+
import activeMQIcon from '../../../../../react-core/src/demos/CardView/../assets/activemq-core_200x150.png';
|
|
8
|
+
import avroIcon from '../../../../../react-core/src/demos/CardView/../assets/camel-avro_200x150.png';
|
|
9
|
+
import dropBoxIcon from '../../../../../react-core/src/demos/CardView/../assets/camel-dropbox_200x150.png';
|
|
10
|
+
import infinispanIcon from '../../../../../react-core/src/demos/CardView/../assets/camel-infinispan_200x150.png';
|
|
11
|
+
import saxonIcon from '../../../../../react-core/src/demos/CardView/../assets/camel-saxon_200x150.png';
|
|
12
|
+
import sparkIcon from '../../../../../react-core/src/demos/CardView/../assets/camel-spark_200x150.png';
|
|
13
|
+
import swaggerIcon from '../../../../../react-core/src/demos/CardView/../assets/camel-swagger-java_200x150.png';
|
|
14
|
+
import azureIcon from '../../../../../react-core/src/demos/CardView/../assets/FuseConnector_Icons_AzureServices.png';
|
|
15
|
+
import restIcon from '../../../../../react-core/src/demos/CardView/../assets/FuseConnector_Icons_REST.png';
|
|
16
|
+
import RhUiEllipsisVerticalFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ellipsis-vertical-fill-icon';
|
|
17
|
+
import { data } from '@patternfly/react-core/src/demos/CardView/examples/CardViewData.jsx';
|
|
18
|
+
import { DashboardWrapper } from '@patternfly/react-core/dist/js/demos/DashboardWrapper';
|
|
19
|
+
import srcImportcardview from './react-demos/card-view.png';
|
|
20
|
+
const pageData = {
|
|
21
|
+
"id": "Card view",
|
|
22
|
+
"section": "patterns",
|
|
23
|
+
"subsection": "",
|
|
24
|
+
"deprecated": false,
|
|
25
|
+
"template": false,
|
|
26
|
+
"beta": false,
|
|
27
|
+
"demo": false,
|
|
28
|
+
"newImplementationLink": false,
|
|
29
|
+
"source": "react-demos",
|
|
30
|
+
"tabName": null,
|
|
31
|
+
"slug": "/patterns/card-view/react-demos",
|
|
32
|
+
"sourceLink": "https://github.com/patternfly/patternfly-react/blob/main/packages/react-core/src/demos/CardView/CardView.md",
|
|
33
|
+
"relPath": "packages/react-core/src/demos/CardView/CardView.md",
|
|
34
|
+
"fullscreenExamples": [
|
|
35
|
+
"Card view"
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
pageData.liveContext = {
|
|
39
|
+
Fragment,
|
|
40
|
+
useState,
|
|
41
|
+
TrashIcon,
|
|
42
|
+
RhUiAddCircleFillIcon,
|
|
43
|
+
pfIcon,
|
|
44
|
+
activeMQIcon,
|
|
45
|
+
avroIcon,
|
|
46
|
+
dropBoxIcon,
|
|
47
|
+
infinispanIcon,
|
|
48
|
+
saxonIcon,
|
|
49
|
+
sparkIcon,
|
|
50
|
+
swaggerIcon,
|
|
51
|
+
azureIcon,
|
|
52
|
+
restIcon,
|
|
53
|
+
RhUiEllipsisVerticalFillIcon,
|
|
54
|
+
data,
|
|
55
|
+
DashboardWrapper
|
|
56
|
+
};
|
|
57
|
+
pageData.examples = {
|
|
58
|
+
'Card view': props =>
|
|
59
|
+
<Example {...pageData} {...props} thumbnail={srcImportcardview} {...{"code":"import { Fragment, useState } from 'react';\nimport {\n Badge,\n Bullseye,\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardBody,\n Content,\n Divider,\n Dropdown,\n DropdownItem,\n DropdownList,\n EmptyState,\n EmptyStateFooter,\n EmptyStateVariant,\n EmptyStateActions,\n Gallery,\n MenuToggle,\n MenuToggleCheckbox,\n OverflowMenu,\n OverflowMenuControl,\n OverflowMenuDropdownItem,\n OverflowMenuItem,\n PageSection,\n Pagination,\n Toolbar,\n ToolbarItem,\n ToolbarFilter,\n ToolbarContent,\n Select,\n SelectList,\n SelectOption,\n MenuToggleElement\n} from '@patternfly/react-core';\nimport TrashIcon from '@patternfly/react-icons/dist/esm/icons/trash-icon';\nimport RhUiAddCircleFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-circle-fill-icon';\nimport pfIcon from './assets/pf-logo-small.svg';\nimport activeMQIcon from './assets/activemq-core_200x150.png';\nimport avroIcon from './assets/camel-avro_200x150.png';\nimport dropBoxIcon from './assets/camel-dropbox_200x150.png';\nimport infinispanIcon from './assets/camel-infinispan_200x150.png';\nimport saxonIcon from './assets/camel-saxon_200x150.png';\nimport sparkIcon from './assets/camel-spark_200x150.png';\nimport swaggerIcon from './assets/camel-swagger-java_200x150.png';\nimport azureIcon from './assets/FuseConnector_Icons_AzureServices.png';\nimport restIcon from './assets/FuseConnector_Icons_REST.png';\nimport RhUiEllipsisVerticalFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ellipsis-vertical-fill-icon';\nimport { DashboardWrapper } from '@patternfly/react-core/dist/js/demos/DashboardWrapper';\nimport { data } from '@patternfly/react-core/src/demos/CardView/examples/CardViewData.jsx';\n\nexport const CardViewBasic: React.FunctionComponent = () => {\n const totalItemCount = 10;\n\n const [cardData, setCardData] = useState(data);\n const [isChecked, setIsChecked] = useState(false);\n const [selectedItems, setSelectedItems] = useState<number[]>([]);\n const [areAllSelected, setAreAllSelected] = useState<boolean>(false);\n const [splitButtonDropdownIsOpen, setSplitButtonDropdownIsOpen] = useState(false);\n const [isLowerToolbarDropdownOpen, setIsLowerToolbarDropdownOpen] = useState(false);\n const [isLowerToolbarKebabDropdownOpen, setIsLowerToolbarKebabDropdownOpen] = useState(false);\n const [page, setPage] = useState(1);\n const [perPage, setPerPage] = useState(10);\n const [filters, setFilters] = useState<Record<string, string[]>>({ products: [] });\n const [state, setState] = useState({});\n\n interface ProductType {\n id: number;\n name: string;\n icon: string;\n description: string;\n }\n const checkAllSelected = (selected: number, total: number) => {\n if (selected && selected < total) {\n return null;\n }\n return selected === total;\n };\n\n const onToolbarDropdownToggle = () => {\n setIsLowerToolbarDropdownOpen(!isLowerToolbarDropdownOpen);\n };\n\n const onToolbarKebabDropdownToggle = () => {\n setIsLowerToolbarKebabDropdownOpen(!isLowerToolbarKebabDropdownOpen);\n };\n\n const onToolbarKebabDropdownSelect = () => {\n setIsLowerToolbarKebabDropdownOpen(!isLowerToolbarKebabDropdownOpen);\n };\n\n const onCardKebabDropdownToggle = (\n event: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.MouseEvent<HTMLDivElement, MouseEvent>,\n key: string\n ) => {\n setState({\n [key]: !state[key as keyof Object]\n });\n };\n\n const deleteItem = (item: ProductType) => {\n const filter = (getter) => (val) => getter(val) !== item.id;\n\n setCardData(cardData.filter(filter(({ id }) => id)));\n\n setSelectedItems(selectedItems.filter(filter((id) => id)));\n };\n\n const onSetPage = (_event: any, pageNumber: number) => {\n setPage(pageNumber);\n };\n\n const onPerPageSelect = (_event: any, perPage: number) => {\n setPerPage(perPage);\n setPage(1);\n };\n\n const onSplitButtonToggle = () => {\n setSplitButtonDropdownIsOpen(!splitButtonDropdownIsOpen);\n };\n\n const onSplitButtonSelect = () => {\n setSplitButtonDropdownIsOpen(false);\n };\n\n const onNameSelect = (event: any, selection = '') => {\n const checked = event.target.checked;\n const prevSelections = filters.products;\n\n setFilters({\n ...filters,\n products: checked ? [...prevSelections, selection] : prevSelections.filter((value) => value !== selection)\n });\n };\n\n const onDelete = (type = '', _id = '') => {\n if (type) {\n setFilters(filters);\n } else {\n setFilters({ products: [] });\n }\n };\n\n const onChange = (event: React.FormEvent<HTMLInputElement>) => {\n const name = event.currentTarget.name;\n const productId = Number(name.charAt(name.length - 1));\n\n if (selectedItems.includes(productId * 1)) {\n setSelectedItems(selectedItems.filter((id) => productId * 1 !== id));\n\n const checkAll = checkAllSelected(selectedItems.length - 1, totalItemCount);\n setAreAllSelected(!!checkAll);\n } else {\n setSelectedItems([...selectedItems, productId * 1]);\n const checkAll = checkAllSelected(selectedItems.length + 1, totalItemCount);\n setAreAllSelected(!!checkAll);\n }\n };\n\n const updateSelected = () => {\n const rows = cardData.map((post) => {\n post.selected = selectedItems.includes(post.id);\n return post;\n });\n\n setCardData(rows);\n };\n\n const getAllItems = () => {\n const collection: number[] = [];\n for (const items of cardData) {\n collection.push(items.id);\n }\n\n return collection;\n };\n\n const splitCheckboxSelectAll = (e: any) => {\n let collection: number[] = [];\n\n if (e.target.checked) {\n for (let i = 0; i <= 9; i++) {\n collection = [...collection, i];\n }\n }\n\n setSelectedItems(collection);\n setIsChecked(isChecked);\n setAreAllSelected(e.target.checked);\n\n updateSelected();\n };\n\n const selectPage = (e: { target: { checked: any } }) => {\n const { checked } = e.target;\n let collection: number[] = [];\n\n collection = getAllItems();\n\n setSelectedItems(collection);\n setIsChecked(checked);\n setAreAllSelected(totalItemCount === perPage ? true : false);\n\n updateSelected();\n };\n\n const selectAll = () => {\n let collection: number[] = [];\n for (let i = 0; i <= 9; i++) {\n collection = [...collection, i];\n }\n\n setSelectedItems(collection);\n setIsChecked(true);\n setAreAllSelected(true);\n\n updateSelected();\n };\n\n const selectNone = () => {\n setSelectedItems([]);\n setIsChecked(false);\n setAreAllSelected(false);\n\n updateSelected();\n };\n\n const renderPagination = () => {\n const defaultPerPageOptions = [\n {\n title: '1',\n value: 1\n },\n {\n title: '5',\n value: 5\n },\n {\n title: '10',\n value: 10\n }\n ];\n\n return (\n <Pagination\n itemCount={totalItemCount}\n page={page}\n perPage={perPage}\n perPageOptions={defaultPerPageOptions}\n onSetPage={onSetPage}\n onPerPageSelect={onPerPageSelect}\n variant=\"top\"\n isCompact\n />\n );\n };\n\n const buildSelectDropdown = () => {\n const numSelected = selectedItems.length;\n const anySelected = numSelected > 0;\n const splitButtonDropdownItems = (\n <>\n <DropdownItem key=\"item-1\" onClick={selectNone}>\n Select none (0 items)\n </DropdownItem>\n <DropdownItem key=\"item-2\" onClick={selectPage}>\n Select page ({perPage} items)\n </DropdownItem>\n <DropdownItem key=\"item-3\" onClick={selectAll}>\n Select all ({totalItemCount} items)\n </DropdownItem>\n </>\n );\n return (\n <Dropdown\n onSelect={onSplitButtonSelect}\n isOpen={splitButtonDropdownIsOpen}\n onOpenChange={(isOpen) => setSplitButtonDropdownIsOpen(isOpen)}\n toggle={(toggleRef) => (\n <MenuToggle\n ref={toggleRef}\n isExpanded={splitButtonDropdownIsOpen}\n onClick={onSplitButtonToggle}\n aria-label=\"Select cards\"\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"split-dropdown-checkbox\"\n key=\"split-dropdown-checkbox\"\n aria-label={anySelected ? 'Deselect all cards' : 'Select all cards'}\n isChecked={areAllSelected}\n onClick={(e) => splitCheckboxSelectAll(e)}\n >\n {numSelected !== 0 && `${numSelected} selected`}\n </MenuToggleCheckbox>\n ]}\n ></MenuToggle>\n )}\n >\n <DropdownList>{splitButtonDropdownItems}</DropdownList>\n </Dropdown>\n );\n };\n\n const buildFilterDropdown = () => {\n const filterDropdownItems = (\n <SelectList>\n <SelectOption\n hasCheckbox\n key=\"patternfly\"\n value=\"PatternFly\"\n isSelected={filters.products.includes('PatternFly')}\n >\n PatternFly\n </SelectOption>\n <SelectOption hasCheckbox key=\"activemq\" value=\"ActiveMQ\" isSelected={filters.products.includes('ActiveMQ')}>\n ActiveMQ\n </SelectOption>\n <SelectOption\n hasCheckbox\n key=\"apachespark\"\n value=\"Apache Spark\"\n isSelected={filters.products.includes('Apache Spark')}\n >\n Apache Spark\n </SelectOption>\n <SelectOption hasCheckbox key=\"avro\" value=\"Avro\" isSelected={filters.products.includes('Avro')}>\n Avro\n </SelectOption>\n <SelectOption\n hasCheckbox\n key=\"azureservices\"\n value=\"Azure Services\"\n isSelected={filters.products.includes('Azure Services')}\n >\n Azure Services\n </SelectOption>\n <SelectOption hasCheckbox key=\"crypto\" value=\"Crypto\" isSelected={filters.products.includes('Crypto')}>\n Crypto\n </SelectOption>\n <SelectOption hasCheckbox key=\"dropbox\" value=\"DropBox\" isSelected={filters.products.includes('DropBox')}>\n DropBox\n </SelectOption>\n <SelectOption\n hasCheckbox\n key=\"jbossdatagrid\"\n value=\"JBoss Data Grid\"\n isSelected={filters.products.includes('JBoss Data Grid')}\n >\n JBoss Data Grid\n </SelectOption>\n <SelectOption hasCheckbox key=\"rest\" value=\"REST\" isSelected={filters.products.includes('REST')}>\n REST\n </SelectOption>\n <SelectOption hasCheckbox key=\"swagger\" value=\"SWAGGER\" isSelected={filters.products.includes('SWAGGER')}>\n SWAGGER\n </SelectOption>\n </SelectList>\n );\n\n return (\n <ToolbarFilter\n categoryName=\"Products\"\n labels={filters.products}\n deleteLabel={(type, id) => onDelete(type as string, id as string)}\n >\n <Select\n aria-label=\"Products\"\n role=\"menu\"\n toggle={(toggleRef) => (\n <MenuToggle ref={toggleRef} onClick={onToolbarDropdownToggle} isExpanded={isLowerToolbarDropdownOpen}>\n Filter by creator name\n {filters.products.length > 0 && <Badge isRead>{filters.products.length}</Badge>}\n </MenuToggle>\n )}\n onSelect={(event, selection) => onNameSelect(event, selection.toString())}\n onOpenChange={(isOpen) => {\n setIsLowerToolbarDropdownOpen(isOpen);\n }}\n selected={filters.products}\n isOpen={isLowerToolbarDropdownOpen}\n >\n {filterDropdownItems}\n </Select>\n </ToolbarFilter>\n );\n };\n\n const toolbarKebabDropdownItems = [\n <OverflowMenuDropdownItem itemId={0} key=\"link\">\n Link\n </OverflowMenuDropdownItem>,\n <OverflowMenuDropdownItem itemId={1} key=\"action\" component=\"button\">\n Action\n </OverflowMenuDropdownItem>,\n <OverflowMenuDropdownItem itemId={2} key=\"disabled link\" isDisabled>\n Disabled Link\n </OverflowMenuDropdownItem>,\n <OverflowMenuDropdownItem itemId={3} key=\"disabled action\" isDisabled component=\"button\">\n Disabled Action\n </OverflowMenuDropdownItem>,\n <Divider key=\"separator\" />,\n <OverflowMenuDropdownItem itemId={5} key=\"separated link\">\n Separated Link\n </OverflowMenuDropdownItem>,\n <OverflowMenuDropdownItem itemId={6} key=\"separated action\" component=\"button\">\n Separated Action\n </OverflowMenuDropdownItem>\n ];\n\n const toolbarItems = (\n <Fragment>\n <ToolbarItem>{buildSelectDropdown()}</ToolbarItem>\n <ToolbarItem>{buildFilterDropdown()}</ToolbarItem>\n <ToolbarItem>\n <OverflowMenu breakpoint=\"md\">\n <OverflowMenuItem>\n <Button variant=\"primary\">Create a project</Button>\n </OverflowMenuItem>\n <OverflowMenuControl hasAdditionalOptions>\n <Dropdown\n onSelect={onToolbarKebabDropdownSelect}\n toggle={(toggleRef) => (\n <MenuToggle\n ref={toggleRef}\n aria-label=\"Toolbar kebab overflow menu\"\n variant=\"plain\"\n onClick={onToolbarKebabDropdownToggle}\n isExpanded={isLowerToolbarKebabDropdownOpen}\n icon={<RhUiEllipsisVerticalFillIcon />}\n />\n )}\n isOpen={isLowerToolbarKebabDropdownOpen}\n onOpenChange={(isOpen) => setIsLowerToolbarDropdownOpen(isOpen)}\n >\n <DropdownList>{toolbarKebabDropdownItems}</DropdownList>\n </Dropdown>\n </OverflowMenuControl>\n </OverflowMenu>\n </ToolbarItem>\n <ToolbarItem variant=\"pagination\" align={{ default: 'alignEnd' }}>\n {renderPagination()}\n </ToolbarItem>\n </Fragment>\n );\n\n const icons = {\n pfIcon,\n activeMQIcon,\n sparkIcon,\n avroIcon,\n azureIcon,\n saxonIcon,\n dropBoxIcon,\n infinispanIcon,\n restIcon,\n swaggerIcon\n };\n\n const filtered =\n filters.products.length > 0\n ? data.filter((card: { name: string }) => filters.products.length === 0 || filters.products.includes(card.name))\n : cardData.slice((page - 1) * perPage, perPage === 1 ? page * perPage : page * perPage - 1);\n\n return (\n <Fragment>\n <DashboardWrapper mainContainerId=\"main-content-card-view-default-nav\" breadcrumb={null}>\n <PageSection aria-labelledby=\"projects\">\n <Content>\n <h1 id=\"projects\">Projects</h1>\n <p>This is a demo that showcases PatternFly cards.</p>\n </Content>\n <Toolbar id=\"toolbar-group-types\" clearAllFilters={onDelete}>\n <ToolbarContent>{toolbarItems}</ToolbarContent>\n </Toolbar>\n </PageSection>\n <PageSection isFilled aria-label=\"Selectable card gallery\">\n <Gallery hasGutter aria-label=\"Selectable card container\">\n <Card isCompact>\n <Bullseye>\n <EmptyState\n headingLevel=\"h2\"\n titleText=\"Add a new card to your page\"\n icon={RhUiAddCircleFillIcon}\n variant={EmptyStateVariant.xs}\n >\n <EmptyStateFooter>\n <EmptyStateActions>\n <Button variant=\"link\">Add card</Button>\n </EmptyStateActions>\n </EmptyStateFooter>\n </EmptyState>\n </Bullseye>\n </Card>\n {filtered.map((product, key) => (\n <Card isCompact isClickable isSelectable key={product.name} id={product.name.replace(/ /g, '-')}>\n <CardHeader\n selectableActions={{\n isChecked: selectedItems.includes(product.id),\n selectableActionId: `selectable-actions-item-${product.id}`,\n selectableActionAriaLabelledby: product.name.replace(/ /g, '-'),\n name: `check-${product.id}`,\n onChange\n }}\n actions={{\n actions: (\n <>\n <Dropdown\n isOpen={!!state[key] ?? false}\n onOpenChange={(isOpen) => setState({ [key]: isOpen })}\n toggle={(toggleRef: React.Ref<MenuToggleElement>) => (\n <MenuToggle\n ref={toggleRef}\n aria-label={`${product.name} actions`}\n variant=\"plain\"\n onClick={(e) => {\n onCardKebabDropdownToggle(e, key.toString());\n }}\n isExpanded={!!state[key]}\n icon={<RhUiEllipsisVerticalFillIcon />}\n />\n )}\n popperProps={{ position: 'right' }}\n >\n <DropdownList>\n <DropdownItem\n key=\"trash\"\n onClick={() => {\n deleteItem(product);\n }}\n >\n <TrashIcon />\n Delete\n </DropdownItem>\n </DropdownList>\n </Dropdown>\n </>\n )\n }}\n >\n <img src={icons[product.icon]} alt={`${product.name} icon`} style={{ maxWidth: '60px' }} />\n </CardHeader>\n <CardTitle>{product.name}</CardTitle>\n <CardBody>{product.description}</CardBody>\n </Card>\n ))}\n </Gallery>\n </PageSection>\n <PageSection\n isFilled={false}\n stickyOnBreakpoint={{ default: 'bottom' }}\n padding={{ default: 'noPadding' }}\n aria-label=\"Pagination controls\"\n >\n <Pagination\n itemCount={totalItemCount}\n page={page}\n perPage={perPage}\n onPerPageSelect={onPerPageSelect}\n onSetPage={onSetPage}\n variant=\"bottom\"\n />\n </PageSection>\n </DashboardWrapper>\n </Fragment>\n );\n};\n","title":"Card view","lang":"ts","isFullscreen":true,"className":""}}>
|
|
60
|
+
|
|
61
|
+
</Example>
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const Component = () => (
|
|
65
|
+
<React.Fragment>
|
|
66
|
+
<AutoLinkHeader {...{"id":"demos","headingLevel":"h2","className":"ws-title ws-h2"}}>
|
|
67
|
+
{`Demos`}
|
|
68
|
+
</AutoLinkHeader>
|
|
69
|
+
<p {...{"className":"pf-v6-c-content--p pf-m-editorial ws-p "}}>
|
|
70
|
+
{`This demonstrates how you can assemble a full page view that contains a grid of equal sized cards that includes a toolbar for managing card grid contents.`}
|
|
71
|
+
</p>
|
|
72
|
+
{React.createElement(pageData.examples["Card view"])}
|
|
73
|
+
</React.Fragment>
|
|
74
|
+
);
|
|
75
|
+
Component.displayName = 'PatternsCardViewReactDemosDocs';
|
|
76
|
+
Component.pageData = pageData;
|
|
77
|
+
|
|
78
|
+
export default Component;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AutoLinkHeader, Example, Link as PatternflyThemeLink } from '@patternfly/documentation-framework/components';
|
|
3
|
+
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Popper,
|
|
6
|
+
SearchInput,
|
|
7
|
+
Toolbar,
|
|
8
|
+
ToolbarContent,
|
|
9
|
+
ToolbarItem,
|
|
10
|
+
Menu,
|
|
11
|
+
MenuContent,
|
|
12
|
+
MenuList,
|
|
13
|
+
MenuItem,
|
|
14
|
+
MenuToggle,
|
|
15
|
+
MenuToggleCheckbox,
|
|
16
|
+
ToolbarGroup,
|
|
17
|
+
ToolbarFilter,
|
|
18
|
+
Badge,
|
|
19
|
+
Pagination,
|
|
20
|
+
EmptyState,
|
|
21
|
+
EmptyStateBody,
|
|
22
|
+
EmptyStateFooter,
|
|
23
|
+
EmptyStateActions,
|
|
24
|
+
Title,
|
|
25
|
+
Button,
|
|
26
|
+
Bullseye,
|
|
27
|
+
ToolbarToggleGroup
|
|
28
|
+
} from '@patternfly/react-core';
|
|
29
|
+
import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';
|
|
30
|
+
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
|
|
31
|
+
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
|
|
32
|
+
const pageData = {
|
|
33
|
+
"id": "Filters",
|
|
34
|
+
"section": "patterns",
|
|
35
|
+
"subsection": "",
|
|
36
|
+
"deprecated": false,
|
|
37
|
+
"template": false,
|
|
38
|
+
"beta": false,
|
|
39
|
+
"demo": false,
|
|
40
|
+
"newImplementationLink": false,
|
|
41
|
+
"source": "react-demos",
|
|
42
|
+
"tabName": null,
|
|
43
|
+
"slug": "/patterns/filters/react-demos",
|
|
44
|
+
"sourceLink": "https://github.com/patternfly/patternfly-react/blob/main/packages/react-core/src/demos/Filters/FilterDemos.md",
|
|
45
|
+
"relPath": "packages/react-core/src/demos/Filters/FilterDemos.md",
|
|
46
|
+
"examples": [
|
|
47
|
+
"Search input",
|
|
48
|
+
"Single select",
|
|
49
|
+
"Checkbox select",
|
|
50
|
+
"Attribute search",
|
|
51
|
+
"Mixed select filter group",
|
|
52
|
+
"Single select filter group",
|
|
53
|
+
"Faceted filter"
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
pageData.liveContext = {
|
|
57
|
+
Fragment,
|
|
58
|
+
useEffect,
|
|
59
|
+
useRef,
|
|
60
|
+
useState,
|
|
61
|
+
Popper,
|
|
62
|
+
SearchInput,
|
|
63
|
+
Toolbar,
|
|
64
|
+
ToolbarContent,
|
|
65
|
+
ToolbarItem,
|
|
66
|
+
Menu,
|
|
67
|
+
MenuContent,
|
|
68
|
+
MenuList,
|
|
69
|
+
MenuItem,
|
|
70
|
+
MenuToggle,
|
|
71
|
+
MenuToggleCheckbox,
|
|
72
|
+
ToolbarGroup,
|
|
73
|
+
ToolbarFilter,
|
|
74
|
+
Badge,
|
|
75
|
+
Pagination,
|
|
76
|
+
EmptyState,
|
|
77
|
+
EmptyStateBody,
|
|
78
|
+
EmptyStateFooter,
|
|
79
|
+
EmptyStateActions,
|
|
80
|
+
Title,
|
|
81
|
+
Button,
|
|
82
|
+
Bullseye,
|
|
83
|
+
ToolbarToggleGroup,
|
|
84
|
+
FilterIcon,
|
|
85
|
+
SearchIcon,
|
|
86
|
+
Table,
|
|
87
|
+
Thead,
|
|
88
|
+
Tr,
|
|
89
|
+
Th,
|
|
90
|
+
Tbody,
|
|
91
|
+
Td
|
|
92
|
+
};
|
|
93
|
+
pageData.examples = {
|
|
94
|
+
'Search input': props =>
|
|
95
|
+
<Example {...pageData} {...props} {...{"code":"import { Fragment, useEffect, useRef, useState } from 'react';\nimport {\n SearchInput,\n Toolbar,\n ToolbarContent,\n ToolbarItem,\n Menu,\n MenuContent,\n MenuList,\n MenuItem,\n MenuToggle,\n MenuToggleCheckbox,\n Popper,\n Pagination,\n EmptyState,\n EmptyStateFooter,\n EmptyStateBody,\n Button,\n Bullseye,\n EmptyStateActions\n} from '@patternfly/react-core';\nimport { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';\nimport SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';\n\ninterface Repository {\n name: string;\n threads: string;\n apps: string;\n workspaces: string;\n status: string;\n location: string;\n}\n\n// In real usage, this data would come from some external source like an API via props.\nconst repositories: Repository[] = [\n { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },\n { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },\n { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },\n { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },\n { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },\n { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },\n { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },\n { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },\n { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },\n { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }\n];\n\nconst columnNames = {\n name: 'Servers',\n threads: 'Threads',\n apps: 'Applications',\n workspaces: 'Workspaces',\n status: 'Status',\n location: 'Location'\n};\n\nexport const FilterSearchInput: React.FunctionComponent = () => {\n // Set up repo filtering\n const [searchValue, setSearchValue] = useState('');\n\n const onSearchChange = (value: string) => {\n setSearchValue(value);\n };\n\n const onFilter = (repo: Repository) => {\n if (searchValue === '') {\n return true;\n }\n\n let input: RegExp;\n try {\n input = new RegExp(searchValue, 'i');\n } catch (err) {\n input = new RegExp(searchValue.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'i');\n }\n return repo.name.search(input) >= 0;\n };\n const filteredRepos = repositories.filter(onFilter);\n\n // Set up table row selection\n // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.\n // This is to prevent state from being based on row order index in case we later add sorting.\n const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example\n const [selectedRepoNames, setSelectedRepoNames] = useState<string[]>([]);\n const setRepoSelected = (repo: Repository, isSelecting = true) =>\n setSelectedRepoNames((prevSelected) => {\n const otherSelectedRepoNames = prevSelected.filter((r) => r !== repo.name);\n return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;\n });\n const selectAllRepos = (isSelecting = true) =>\n setSelectedRepoNames(isSelecting ? filteredRepos.map((r) => r.name) : []); // Selecting all should only select all currently filtered rows\n const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;\n const areSomeReposSelected = selectedRepoNames.length > 0;\n const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);\n\n // To allow shift+click to select/deselect multiple rows\n const [recentSelectedRowIndex, setRecentSelectedRowIndex] = useState<number | null>(null);\n const [shifting, setShifting] = useState(false);\n\n const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {\n // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected\n if (shifting && recentSelectedRowIndex !== null) {\n const numberSelected = rowIndex - recentSelectedRowIndex;\n const intermediateIndexes =\n numberSelected > 0\n ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)\n : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);\n intermediateIndexes.forEach((index) => setRepoSelected(repositories[index], isSelecting));\n } else {\n setRepoSelected(repo, isSelecting);\n }\n setRecentSelectedRowIndex(rowIndex);\n };\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(true);\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(false);\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('keyup', onKeyUp);\n\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('keyup', onKeyUp);\n };\n }, []);\n\n // Set up bulk selection menu\n const bulkSelectMenuRef = useRef<HTMLDivElement>(null);\n const bulkSelectToggleRef = useRef<any>(null);\n const bulkSelectContainerRef = useRef<HTMLDivElement>(null);\n\n const [isBulkSelectOpen, setIsBulkSelectOpen] = useState<boolean>(false);\n\n const handleBulkSelectClickOutside = (event: MouseEvent) => {\n if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {\n setIsBulkSelectOpen(false);\n }\n };\n\n const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {\n if (!isBulkSelectOpen) {\n return;\n }\n if (\n bulkSelectMenuRef.current?.contains(event.target as Node) ||\n bulkSelectToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleBulkSelectMenuKeys);\n window.addEventListener('click', handleBulkSelectClickOutside);\n return () => {\n window.removeEventListener('keydown', handleBulkSelectMenuKeys);\n window.removeEventListener('click', handleBulkSelectClickOutside);\n };\n }, [isBulkSelectOpen, bulkSelectMenuRef]);\n\n const onBulkSelectToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (bulkSelectMenuRef.current) {\n const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n };\n\n let menuToggleCheckmark: boolean | null = false;\n if (areAllReposSelected) {\n menuToggleCheckmark = true;\n } else if (areSomeReposSelected) {\n menuToggleCheckmark = null;\n }\n\n const bulkSelectToggle = (\n <MenuToggle\n ref={bulkSelectToggleRef}\n onClick={onBulkSelectToggleClick}\n isExpanded={isBulkSelectOpen}\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"search-input-bulk-select\"\n key=\"search-input-bulk-select\"\n aria-label=\"Select all\"\n isChecked={menuToggleCheckmark}\n onChange={(checked, _event) => selectAllRepos(checked)}\n />\n ]}\n aria-label=\"Full table selection checkbox\"\n />\n );\n\n const bulkSelectMenu = (\n <Menu\n id=\"search-input-bulk-select\"\n ref={bulkSelectMenuRef}\n onSelect={(_ev, itemId) => {\n selectAllRepos(itemId === 1 || itemId === 2);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId={0}>Select none (0 items)</MenuItem>\n <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>\n <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const toolbarBulkSelect = (\n <div ref={bulkSelectContainerRef}>\n <Popper\n trigger={bulkSelectToggle}\n triggerRef={bulkSelectToggleRef}\n popper={bulkSelectMenu}\n popperRef={bulkSelectMenuRef}\n appendTo={bulkSelectContainerRef.current || undefined}\n isVisible={isBulkSelectOpen}\n />\n </div>\n );\n\n const searchInput = (\n <SearchInput\n placeholder=\"Filter by server name\"\n value={searchValue}\n onChange={(_event, value) => onSearchChange(value)}\n onClear={() => onSearchChange('')}\n />\n );\n\n const toolbarPagination = (\n <Pagination\n titles={{ paginationAriaLabel: 'Search filter pagination' }}\n itemCount={repositories.length}\n perPage={10}\n page={1}\n widgetId=\"search-input-mock-pagination\"\n isCompact\n />\n );\n\n const toolbar = (\n <Toolbar id=\"search-input-filter-toolbar\">\n <ToolbarContent>\n <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>\n <ToolbarItem>{searchInput}</ToolbarItem>\n <ToolbarItem variant=\"pagination\">{toolbarPagination}</ToolbarItem>\n </ToolbarContent>\n </Toolbar>\n );\n\n const emptyState = (\n <EmptyState headingLevel=\"h4\" titleText=\"No results found\" icon={SearchIcon}>\n <EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>\n <EmptyStateFooter>\n <EmptyStateActions>\n <Button\n variant=\"link\"\n onClick={() => {\n setSearchValue('');\n }}\n >\n Clear all filters\n </Button>\n </EmptyStateActions>\n </EmptyStateFooter>\n </EmptyState>\n );\n\n return (\n <Fragment>\n {toolbar}\n <Table aria-label=\"Selectable table\">\n <Thead>\n <Tr>\n <Th screenReaderText=\"Row select\" />\n <Th width={20}>{columnNames.name}</Th>\n <Th width={10}>{columnNames.threads}</Th>\n <Th width={10}>{columnNames.apps}</Th>\n <Th width={10}>{columnNames.workspaces}</Th>\n <Th width={20}>{columnNames.status}</Th>\n <Th width={20}>{columnNames.location}</Th>\n </Tr>\n </Thead>\n <Tbody>\n {filteredRepos.length > 0 &&\n filteredRepos.map((repo, rowIndex) => (\n <Tr key={repo.name}>\n <Td\n select={{\n rowIndex,\n onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),\n isSelected: isRepoSelected(repo),\n isDisabled: !isRepoSelectable(repo)\n }}\n />\n <Td dataLabel={columnNames.name} modifier=\"truncate\">\n {repo.name}\n </Td>\n <Td dataLabel={columnNames.threads} modifier=\"truncate\">\n {repo.threads}\n </Td>\n <Td dataLabel={columnNames.apps} modifier=\"truncate\">\n {repo.apps}\n </Td>\n <Td dataLabel={columnNames.workspaces} modifier=\"truncate\">\n {repo.workspaces}\n </Td>\n <Td dataLabel={columnNames.status} modifier=\"truncate\">\n {repo.status}\n </Td>\n <Td dataLabel={columnNames.location} modifier=\"truncate\">\n {repo.location}\n </Td>\n </Tr>\n ))}\n {filteredRepos.length === 0 && (\n <Tr>\n <Td colSpan={8}>\n <Bullseye>{emptyState}</Bullseye>\n </Td>\n </Tr>\n )}\n </Tbody>\n </Table>\n </Fragment>\n );\n};\n","title":"Search input","lang":"ts","className":""}}>
|
|
96
|
+
|
|
97
|
+
</Example>,
|
|
98
|
+
'Single select': props =>
|
|
99
|
+
<Example {...pageData} {...props} {...{"code":"import { Fragment, useEffect, useRef, useState } from 'react';\nimport {\n Menu,\n MenuContent,\n MenuList,\n MenuItem,\n MenuToggle,\n MenuToggleCheckbox,\n Popper,\n Toolbar,\n ToolbarContent,\n ToolbarItem,\n Pagination\n} from '@patternfly/react-core';\nimport { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';\nimport FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';\n\ninterface Repository {\n name: string;\n threads: string;\n apps: string;\n workspaces: string;\n status: string;\n location: string;\n}\n\n// In real usage, this data would come from some external source like an API via props.\nconst repositories: Repository[] = [\n { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },\n { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },\n { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },\n { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },\n { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },\n { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },\n { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },\n { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },\n { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },\n { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }\n];\n\nconst columnNames = {\n name: 'Servers',\n threads: 'Threads',\n apps: 'Applications',\n workspaces: 'Workspaces',\n status: 'Status',\n location: 'Location'\n};\n\nexport const FilterSingleSelect: React.FunctionComponent = () => {\n // Set up repo filtering\n const [searchValue, setSearchValue] = useState('');\n const onFilter = (repo: Repository) => {\n if (searchValue === 'all') {\n return true;\n }\n\n let input: RegExp;\n try {\n input = new RegExp(searchValue, 'i');\n } catch (err) {\n input = new RegExp(searchValue.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'i');\n }\n return repo.status.search(input) >= 0;\n };\n\n const filteredRepos = repositories.filter(onFilter);\n\n // Set up table selection\n // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.\n // This is to prevent state from being based on row order index in case we later add sorting.\n const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example\n const [selectedRepoNames, setSelectedRepoNames] = useState<string[]>([]);\n const setRepoSelected = (repo: Repository, isSelecting = true) =>\n setSelectedRepoNames((prevSelected) => {\n const otherSelectedRepoNames = prevSelected.filter((r) => r !== repo.name);\n return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;\n });\n const selectAllRepos = (isSelecting = true) =>\n setSelectedRepoNames(isSelecting ? filteredRepos.map((r) => r.name) : []);\n const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;\n const areSomeReposSelected = selectedRepoNames.length > 0;\n const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);\n\n // To allow shift+click to select/deselect multiple rows\n const [recentSelectedRowIndex, setRecentSelectedRowIndex] = useState<number | null>(null);\n const [shifting, setShifting] = useState(false);\n\n const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {\n // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected\n if (shifting && recentSelectedRowIndex !== null) {\n const numberSelected = rowIndex - recentSelectedRowIndex;\n const intermediateIndexes =\n numberSelected > 0\n ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)\n : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);\n intermediateIndexes.forEach((index) => setRepoSelected(repositories[index], isSelecting));\n } else {\n setRepoSelected(repo, isSelecting);\n }\n setRecentSelectedRowIndex(rowIndex);\n };\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(true);\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(false);\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('keyup', onKeyUp);\n\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('keyup', onKeyUp);\n };\n }, []);\n\n // Set up bulk selection menu\n const bulkSelectMenuRef = useRef<HTMLDivElement>(null);\n const bulkSelectToggleRef = useRef<any>(null);\n const bulkSelectContainerRef = useRef<HTMLDivElement>(null);\n\n const [isBulkSelectOpen, setIsBulkSelectOpen] = useState<boolean>(false);\n\n const handleBulkSelectClickOutside = (event: MouseEvent) => {\n if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {\n setIsBulkSelectOpen(false);\n }\n };\n\n const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {\n if (!isBulkSelectOpen) {\n return;\n }\n if (\n bulkSelectMenuRef.current?.contains(event.target as Node) ||\n bulkSelectToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleBulkSelectMenuKeys);\n window.addEventListener('click', handleBulkSelectClickOutside);\n return () => {\n window.removeEventListener('keydown', handleBulkSelectMenuKeys);\n window.removeEventListener('click', handleBulkSelectClickOutside);\n };\n }, [isBulkSelectOpen, bulkSelectMenuRef]);\n\n const onBulkSelectToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (bulkSelectMenuRef.current) {\n const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n };\n\n let menuToggleCheckmark: boolean | null = false;\n if (areAllReposSelected) {\n menuToggleCheckmark = true;\n } else if (areSomeReposSelected) {\n menuToggleCheckmark = null;\n }\n\n const bulkSelectToggle = (\n <MenuToggle\n ref={bulkSelectToggleRef}\n onClick={onBulkSelectToggleClick}\n isExpanded={isBulkSelectOpen}\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"single-bulk-select\"\n key=\"single-bulk-select\"\n aria-label=\"Select all\"\n isChecked={menuToggleCheckmark}\n onChange={(checked, _event) => selectAllRepos(checked)}\n />\n ]}\n aria-label=\"Full table selection checkbox\"\n />\n );\n\n const bulkSelectMenu = (\n <Menu\n ref={bulkSelectMenuRef}\n onSelect={(_ev, itemId) => {\n selectAllRepos(itemId === 1 || itemId === 2);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId={0}>Select none (0 items)</MenuItem>\n <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>\n <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const toolbarBulkSelect = (\n <div ref={bulkSelectContainerRef}>\n <Popper\n trigger={bulkSelectToggle}\n triggerRef={bulkSelectToggleRef}\n popper={bulkSelectMenu}\n popperRef={bulkSelectMenuRef}\n appendTo={bulkSelectContainerRef.current || undefined}\n isVisible={isBulkSelectOpen}\n />\n </div>\n );\n\n // Set up single select menu & state\n const [isOpen, setIsOpen] = useState<boolean>(false);\n const [menuSelection, setMenuSelection] = useState<string>('all');\n const toggleRef = useRef<HTMLButtonElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const handleMenuKeys = (event: KeyboardEvent) => {\n if (isOpen && menuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsOpen(!isOpen);\n toggleRef.current?.focus();\n }\n }\n };\n\n const handleClickOutside = (event: MouseEvent) => {\n if (isOpen && !menuRef.current?.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleMenuKeys);\n window.addEventListener('click', handleClickOutside);\n return () => {\n window.removeEventListener('keydown', handleMenuKeys);\n window.removeEventListener('click', handleClickOutside);\n };\n }, [isOpen, menuRef]);\n\n const onToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (menuRef.current) {\n const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsOpen(!isOpen);\n };\n\n function onSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n setMenuSelection(itemId.toString());\n setSearchValue(itemId.toString());\n setIsOpen(!isOpen);\n }\n\n const menuToggleDisplay = {\n all: 'All statuses',\n down: 'Down',\n stopped: 'Stopped',\n degraded: 'Degraded',\n running: 'Running',\n maint: 'Needs maintenance'\n };\n\n const toggle = (\n <MenuToggle\n ref={toggleRef}\n onClick={onToggleClick}\n isExpanded={isOpen}\n icon={<FilterIcon />}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n {menuToggleDisplay[menuSelection]}\n </MenuToggle>\n );\n\n const menu = (\n <Menu ref={menuRef} id=\"single-select-menu\" onSelect={onSelect} selected={menuSelection}>\n <MenuContent>\n <MenuList>\n <MenuItem itemId=\"all\">All statuses</MenuItem>\n <MenuItem itemId=\"stopped\">Stopped</MenuItem>\n <MenuItem itemId=\"down\">Down</MenuItem>\n <MenuItem itemId=\"degraded\">Degraded</MenuItem>\n <MenuItem itemId=\"running\">Running</MenuItem>\n <MenuItem itemId=\"maint\">Needs maintenance</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const select = (\n <div ref={containerRef}>\n <Popper\n trigger={toggle}\n triggerRef={toggleRef}\n popper={menu}\n popperRef={menuRef}\n appendTo={containerRef.current || undefined}\n isVisible={isOpen}\n />\n </div>\n );\n\n // Set up pagination\n const toolbarPagination = (\n <Pagination\n titles={{ paginationAriaLabel: 'Single select filter pagination' }}\n itemCount={repositories.length}\n perPage={10}\n page={1}\n widgetId=\"single-select-mock-pagination\"\n isCompact\n />\n );\n\n const toolbar = (\n <Toolbar id=\"single-select-filter-toolbar\">\n <ToolbarContent>\n <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>\n <ToolbarItem>{select}</ToolbarItem>\n <ToolbarItem variant=\"pagination\">{toolbarPagination}</ToolbarItem>\n </ToolbarContent>\n </Toolbar>\n );\n\n return (\n <Fragment>\n {toolbar}\n <Table aria-label=\"Selectable table\">\n <Thead>\n <Tr>\n <Th screenReaderText=\"Row select\" />\n <Th width={20}>{columnNames.name}</Th>\n <Th width={10}>{columnNames.threads}</Th>\n <Th width={10}>{columnNames.apps}</Th>\n <Th width={10}>{columnNames.workspaces}</Th>\n <Th width={20}>{columnNames.status}</Th>\n <Th width={20}>{columnNames.location}</Th>\n </Tr>\n </Thead>\n <Tbody>\n {filteredRepos.map((repo, rowIndex) => (\n <Tr key={repo.name}>\n <Td\n select={{\n rowIndex,\n onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),\n isSelected: isRepoSelected(repo),\n isDisabled: !isRepoSelectable(repo)\n }}\n />\n <Td dataLabel={columnNames.name} modifier=\"truncate\">\n {repo.name}\n </Td>\n <Td dataLabel={columnNames.threads} modifier=\"truncate\">\n {repo.threads}\n </Td>\n <Td dataLabel={columnNames.apps} modifier=\"truncate\">\n {repo.apps}\n </Td>\n <Td dataLabel={columnNames.workspaces} modifier=\"truncate\">\n {repo.workspaces}\n </Td>\n <Td dataLabel={columnNames.status} modifier=\"truncate\">\n {repo.status}\n </Td>\n <Td dataLabel={columnNames.location} modifier=\"truncate\">\n {repo.location}\n </Td>\n </Tr>\n ))}\n </Tbody>\n </Table>\n </Fragment>\n );\n};\n","title":"Single select","lang":"ts","className":""}}>
|
|
100
|
+
|
|
101
|
+
</Example>,
|
|
102
|
+
'Checkbox select': props =>
|
|
103
|
+
<Example {...pageData} {...props} {...{"code":"import { Fragment, useEffect, useRef, useState } from 'react';\nimport {\n Menu,\n MenuContent,\n MenuList,\n MenuItem,\n MenuToggle,\n MenuToggleCheckbox,\n Popper,\n Toolbar,\n ToolbarContent,\n ToolbarGroup,\n ToolbarFilter,\n ToolbarItem,\n Badge,\n Pagination\n} from '@patternfly/react-core';\nimport { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';\nimport FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';\n\ninterface Repository {\n name: string;\n threads: string;\n apps: string;\n workspaces: string;\n status: string;\n location: string;\n}\n\n// In real usage, this data would come from some external source like an API via props.\nconst repositories: Repository[] = [\n { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },\n { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },\n { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },\n { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },\n { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },\n { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },\n { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },\n { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },\n { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },\n { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }\n];\n\nconst columnNames = {\n name: 'Servers',\n threads: 'Threads',\n apps: 'Applications',\n workspaces: 'Workspaces',\n status: 'Status',\n location: 'Location'\n};\n\nexport const FilterCheckboxSelect: React.FunctionComponent = () => {\n // Set up repo filtering\n const [selections, setSelections] = useState<string[]>([]);\n const onFilter = (repo: Repository) => {\n if (selections.length === 0) {\n return true;\n }\n\n return selections.some((searchValue) => {\n let input: RegExp;\n try {\n input = new RegExp(searchValue, 'i');\n } catch (err) {\n input = new RegExp(searchValue.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'i');\n }\n return repo.location.search(input) >= 0;\n });\n };\n\n const filteredRepos = repositories.filter(onFilter);\n\n // Set up table selection\n // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.\n // This is to prevent state from being based on row order index in case we later add sorting.\n const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example\n const [selectedRepoNames, setSelectedRepoNames] = useState<string[]>([]);\n const setRepoSelected = (repo: Repository, isSelecting = true) =>\n setSelectedRepoNames((prevSelected) => {\n const otherSelectedRepoNames = prevSelected.filter((r) => r !== repo.name);\n return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;\n });\n const selectAllRepos = (isSelecting = true) =>\n setSelectedRepoNames(isSelecting ? filteredRepos.map((r) => r.name) : []);\n const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;\n const areSomeReposSelected = selectedRepoNames.length > 0;\n const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);\n\n // To allow shift+click to select/deselect multiple rows\n const [recentSelectedRowIndex, setRecentSelectedRowIndex] = useState<number | null>(null);\n const [shifting, setShifting] = useState(false);\n\n const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {\n // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected\n if (shifting && recentSelectedRowIndex !== null) {\n const numberSelected = rowIndex - recentSelectedRowIndex;\n const intermediateIndexes =\n numberSelected > 0\n ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)\n : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);\n intermediateIndexes.forEach((index) => setRepoSelected(repositories[index], isSelecting));\n } else {\n setRepoSelected(repo, isSelecting);\n }\n setRecentSelectedRowIndex(rowIndex);\n };\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(true);\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(false);\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('keyup', onKeyUp);\n\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('keyup', onKeyUp);\n };\n }, []);\n\n // Set up bulk selection menu\n const bulkSelectMenuRef = useRef<HTMLDivElement>(null);\n const bulkSelectToggleRef = useRef<any>(null);\n const bulkSelectContainerRef = useRef<HTMLDivElement>(null);\n\n const [isBulkSelectOpen, setIsBulkSelectOpen] = useState<boolean>(false);\n\n const handleBulkSelectClickOutside = (event: MouseEvent) => {\n if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {\n setIsBulkSelectOpen(false);\n }\n };\n\n const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {\n if (!isBulkSelectOpen) {\n return;\n }\n if (\n bulkSelectMenuRef.current?.contains(event.target as Node) ||\n bulkSelectToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleBulkSelectMenuKeys);\n window.addEventListener('click', handleBulkSelectClickOutside);\n return () => {\n window.removeEventListener('keydown', handleBulkSelectMenuKeys);\n window.removeEventListener('click', handleBulkSelectClickOutside);\n };\n }, [isBulkSelectOpen, bulkSelectMenuRef]);\n\n const onBulkSelectToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (bulkSelectMenuRef.current) {\n const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n };\n\n let menuToggleCheckmark: boolean | null = false;\n if (areAllReposSelected) {\n menuToggleCheckmark = true;\n } else if (areSomeReposSelected) {\n menuToggleCheckmark = null;\n }\n\n const bulkSelectToggle = (\n <MenuToggle\n ref={bulkSelectToggleRef}\n onClick={onBulkSelectToggleClick}\n isExpanded={isBulkSelectOpen}\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"checkbox-bulk-select\"\n key=\"checkbox-bulk-select\"\n aria-label=\"Select all\"\n isChecked={menuToggleCheckmark}\n onChange={(checked, _event) => selectAllRepos(checked)}\n />\n ]}\n aria-label=\"Full table selection checkbox\"\n />\n );\n\n const bulkSelectMenu = (\n <Menu\n ref={bulkSelectMenuRef}\n onSelect={(_ev, itemId) => {\n selectAllRepos(itemId === 1 || itemId === 2);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId={0}>Select none (0 items)</MenuItem>\n <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>\n <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const toolbarBulkSelect = (\n <div ref={bulkSelectContainerRef}>\n <Popper\n trigger={bulkSelectToggle}\n triggerRef={bulkSelectToggleRef}\n popper={bulkSelectMenu}\n popperRef={bulkSelectMenuRef}\n appendTo={bulkSelectContainerRef.current || undefined}\n isVisible={isBulkSelectOpen}\n />\n </div>\n );\n\n // Set up checkbox select menu\n const [isOpen, setIsOpen] = useState<boolean>(false);\n const toggleRef = useRef<HTMLButtonElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const handleMenuKeys = (event: KeyboardEvent) => {\n if (isOpen && menuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsOpen(!isOpen);\n toggleRef.current?.focus();\n }\n }\n };\n\n const handleClickOutside = (event: MouseEvent) => {\n if (isOpen && !menuRef.current?.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleMenuKeys);\n window.addEventListener('click', handleClickOutside);\n return () => {\n window.removeEventListener('keydown', handleMenuKeys);\n window.removeEventListener('click', handleClickOutside);\n };\n }, [isOpen, menuRef]);\n\n const onToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (menuRef.current) {\n const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsOpen(!isOpen);\n };\n\n function onSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n const itemStr = itemId.toString();\n setSelections(\n selections.includes(itemStr) ? selections.filter((selection) => selection !== itemStr) : [itemStr, ...selections]\n );\n }\n\n const toggle = (\n <MenuToggle\n ref={toggleRef}\n onClick={onToggleClick}\n isExpanded={isOpen}\n icon={<FilterIcon />}\n {...(selections.length > 0 && { badge: <Badge isRead>{selections.length}</Badge> })}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n Location\n </MenuToggle>\n );\n\n const menu = (\n <Menu ref={menuRef} id=\"checkbox-select-menu\" onSelect={onSelect} selected={selections}>\n <MenuContent>\n <MenuList>\n <MenuItem hasCheckbox isSelected={selections.includes('Bangalore')} itemId=\"Bangalore\">\n Bangalore\n </MenuItem>\n <MenuItem hasCheckbox isSelected={selections.includes('Boston')} itemId=\"Boston\">\n Boston\n </MenuItem>\n <MenuItem hasCheckbox isSelected={selections.includes('Brno')} itemId=\"Brno\">\n Brno\n </MenuItem>\n <MenuItem hasCheckbox isSelected={selections.includes('Raleigh')} itemId=\"Raleigh\">\n Raleigh\n </MenuItem>\n <MenuItem hasCheckbox isSelected={selections.includes('Westford')} itemId=\"Westford\">\n Westford\n </MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const select = (\n <div ref={containerRef}>\n <Popper\n trigger={toggle}\n triggerRef={toggleRef}\n popper={menu}\n popperRef={menuRef}\n appendTo={containerRef.current || undefined}\n isVisible={isOpen}\n />\n </div>\n );\n\n // Set up pagination\n const toolbarPagination = (\n <Pagination\n titles={{ paginationAriaLabel: 'Checkbox filter pagination' }}\n itemCount={repositories.length}\n perPage={10}\n page={1}\n widgetId=\"checkbox-select-mock-pagination\"\n isCompact\n />\n );\n\n const toolbar = (\n <Toolbar id=\"checkbox-filter-toolbar\" clearAllFilters={() => setSelections([])}>\n <ToolbarContent>\n <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>\n <ToolbarGroup variant=\"filter-group\">\n <ToolbarFilter\n labels={selections}\n deleteLabel={(category, label) => onSelect(undefined, label as string)}\n deleteLabelGroup={() => setSelections([])}\n categoryName=\"Location\"\n >\n {select}\n </ToolbarFilter>\n </ToolbarGroup>\n <ToolbarItem variant=\"pagination\">{toolbarPagination}</ToolbarItem>\n </ToolbarContent>\n </Toolbar>\n );\n\n return (\n <Fragment>\n {toolbar}\n <Table aria-label=\"Selectable table\">\n <Thead>\n <Tr>\n <Th screenReaderText=\"Row select\" />\n <Th width={20}>{columnNames.name}</Th>\n <Th width={10}>{columnNames.threads}</Th>\n <Th width={10}>{columnNames.apps}</Th>\n <Th width={10}>{columnNames.workspaces}</Th>\n <Th width={20}>{columnNames.status}</Th>\n <Th width={20}>{columnNames.location}</Th>\n </Tr>\n </Thead>\n <Tbody>\n {filteredRepos.map((repo, rowIndex) => (\n <Tr key={repo.name}>\n <Td\n select={{\n rowIndex,\n onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),\n isSelected: isRepoSelected(repo),\n isDisabled: !isRepoSelectable(repo)\n }}\n />\n <Td dataLabel={columnNames.name} modifier=\"truncate\">\n {repo.name}\n </Td>\n <Td dataLabel={columnNames.threads} modifier=\"truncate\">\n {repo.threads}\n </Td>\n <Td dataLabel={columnNames.apps} modifier=\"truncate\">\n {repo.apps}\n </Td>\n <Td dataLabel={columnNames.workspaces} modifier=\"truncate\">\n {repo.workspaces}\n </Td>\n <Td dataLabel={columnNames.status} modifier=\"truncate\">\n {repo.status}\n </Td>\n <Td dataLabel={columnNames.location} modifier=\"truncate\">\n {repo.location}\n </Td>\n </Tr>\n ))}\n </Tbody>\n </Table>\n </Fragment>\n );\n};\n","title":"Checkbox select","lang":"ts","className":""}}>
|
|
104
|
+
|
|
105
|
+
</Example>,
|
|
106
|
+
'Attribute search': props =>
|
|
107
|
+
<Example {...pageData} {...props} {...{"code":"import { Fragment, useEffect, useRef, useState } from 'react';\nimport {\n SearchInput,\n Toolbar,\n ToolbarContent,\n ToolbarItem,\n Menu,\n MenuContent,\n MenuList,\n MenuItem,\n MenuToggle,\n MenuToggleCheckbox,\n Popper,\n Pagination,\n EmptyState,\n EmptyStateFooter,\n EmptyStateBody,\n Button,\n Bullseye,\n Badge,\n ToolbarGroup,\n ToolbarFilter,\n ToolbarToggleGroup,\n EmptyStateActions\n} from '@patternfly/react-core';\nimport { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';\nimport SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';\nimport FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';\n\ninterface Repository {\n name: string;\n threads: string;\n apps: string;\n workspaces: string;\n status: string;\n location: string;\n}\n\n// In real usage, this data would come from some external source like an API via props.\nconst repositories: Repository[] = [\n { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },\n { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },\n { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },\n { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },\n { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },\n { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },\n { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },\n { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },\n { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },\n { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }\n];\n\nconst columnNames = {\n name: 'Servers',\n threads: 'Threads',\n apps: 'Applications',\n workspaces: 'Workspaces',\n status: 'Status',\n location: 'Location'\n};\n\nexport const FilterAttributeSearch: React.FunctionComponent = () => {\n // Set up repo filtering\n const [searchValue, setSearchValue] = useState('');\n const [locationSelections, setLocationSelections] = useState<string[]>([]);\n const [statusSelection, setStatusSelection] = useState('');\n\n const onSearchChange = (value: string) => {\n setSearchValue(value);\n };\n\n const onFilter = (repo: Repository) => {\n // Search name with search value\n let searchValueInput: RegExp;\n try {\n searchValueInput = new RegExp(searchValue, 'i');\n } catch (err) {\n searchValueInput = new RegExp(searchValue.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'i');\n }\n const matchesSearchValue = repo.name.search(searchValueInput) >= 0;\n\n // Search status with status selection\n const matchesStatusValue = repo.status.toLowerCase() === statusSelection.toLowerCase();\n\n // Search location with location selections\n const matchesLocationValue = locationSelections.includes(repo.location);\n\n return (\n (searchValue === '' || matchesSearchValue) &&\n (statusSelection === '' || matchesStatusValue) &&\n (locationSelections.length === 0 || matchesLocationValue)\n );\n };\n const filteredRepos = repositories.filter(onFilter);\n\n // Set up table row selection\n // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.\n // This is to prevent state from being based on row order index in case we later add sorting.\n const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example\n const [selectedRepoNames, setSelectedRepoNames] = useState<string[]>([]);\n const setRepoSelected = (repo: Repository, isSelecting = true) =>\n setSelectedRepoNames((prevSelected) => {\n const otherSelectedRepoNames = prevSelected.filter((r) => r !== repo.name);\n return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;\n });\n const selectAllRepos = (isSelecting = true) =>\n setSelectedRepoNames(isSelecting ? filteredRepos.map((r) => r.name) : []); // Selecting all should only select all currently filtered rows\n const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;\n const areSomeReposSelected = selectedRepoNames.length > 0;\n const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);\n\n // To allow shift+click to select/deselect multiple rows\n const [recentSelectedRowIndex, setRecentSelectedRowIndex] = useState<number | null>(null);\n const [shifting, setShifting] = useState(false);\n\n const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {\n // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected\n if (shifting && recentSelectedRowIndex !== null) {\n const numberSelected = rowIndex - recentSelectedRowIndex;\n const intermediateIndexes =\n numberSelected > 0\n ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)\n : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);\n intermediateIndexes.forEach((index) => setRepoSelected(repositories[index], isSelecting));\n } else {\n setRepoSelected(repo, isSelecting);\n }\n setRecentSelectedRowIndex(rowIndex);\n };\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(true);\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(false);\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('keyup', onKeyUp);\n\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('keyup', onKeyUp);\n };\n }, []);\n\n // Set up bulk selection menu\n const bulkSelectMenuRef = useRef<HTMLDivElement>(null);\n const bulkSelectToggleRef = useRef<any>(null);\n const bulkSelectContainerRef = useRef<HTMLDivElement>(null);\n\n const [isBulkSelectOpen, setIsBulkSelectOpen] = useState<boolean>(false);\n\n const handleBulkSelectClickOutside = (event: MouseEvent) => {\n if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {\n setIsBulkSelectOpen(false);\n }\n };\n\n const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {\n if (!isBulkSelectOpen) {\n return;\n }\n if (\n bulkSelectMenuRef.current?.contains(event.target as Node) ||\n bulkSelectToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleBulkSelectMenuKeys);\n window.addEventListener('click', handleBulkSelectClickOutside);\n return () => {\n window.removeEventListener('keydown', handleBulkSelectMenuKeys);\n window.removeEventListener('click', handleBulkSelectClickOutside);\n };\n }, [isBulkSelectOpen, bulkSelectMenuRef]);\n\n const onBulkSelectToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (bulkSelectMenuRef.current) {\n const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n };\n\n let menuToggleCheckmark: boolean | null = false;\n if (areAllReposSelected) {\n menuToggleCheckmark = true;\n } else if (areSomeReposSelected) {\n menuToggleCheckmark = null;\n }\n\n const bulkSelectToggle = (\n <MenuToggle\n ref={bulkSelectToggleRef}\n onClick={onBulkSelectToggleClick}\n isExpanded={isBulkSelectOpen}\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"attribute-search-input-bulk-select\"\n key=\"attribute-search-input-bulk-select\"\n aria-label=\"Select all\"\n isChecked={menuToggleCheckmark}\n onChange={(checked, _event) => selectAllRepos(checked)}\n />\n ]}\n aria-label=\"Full table selection checkbox\"\n />\n );\n\n const bulkSelectMenu = (\n <Menu\n id=\"attribute-search-input-bulk-select\"\n ref={bulkSelectMenuRef}\n onSelect={(_ev, itemId) => {\n selectAllRepos(itemId === 1 || itemId === 2);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId={0}>Select none (0 items)</MenuItem>\n <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>\n <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const toolbarBulkSelect = (\n <div ref={bulkSelectContainerRef}>\n <Popper\n trigger={bulkSelectToggle}\n triggerRef={bulkSelectToggleRef}\n popper={bulkSelectMenu}\n popperRef={bulkSelectMenuRef}\n appendTo={bulkSelectContainerRef.current || undefined}\n isVisible={isBulkSelectOpen}\n />\n </div>\n );\n\n // Set up name search input\n const searchInput = (\n <SearchInput\n placeholder=\"Filter by server name\"\n value={searchValue}\n onChange={(_event, value) => onSearchChange(value)}\n onClear={() => onSearchChange('')}\n />\n );\n\n // Set up status single select\n const [isStatusMenuOpen, setIsStatusMenuOpen] = useState<boolean>(false);\n const statusToggleRef = useRef<HTMLButtonElement>(null);\n const statusMenuRef = useRef<HTMLDivElement>(null);\n const statusContainerRef = useRef<HTMLDivElement>(null);\n\n const handleStatusMenuKeys = (event: KeyboardEvent) => {\n if (isStatusMenuOpen && statusMenuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsStatusMenuOpen(!isStatusMenuOpen);\n statusToggleRef.current?.focus();\n }\n }\n };\n\n const handleStatusClickOutside = (event: MouseEvent) => {\n if (isStatusMenuOpen && !statusMenuRef.current?.contains(event.target as Node)) {\n setIsStatusMenuOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleStatusMenuKeys);\n window.addEventListener('click', handleStatusClickOutside);\n return () => {\n window.removeEventListener('keydown', handleStatusMenuKeys);\n window.removeEventListener('click', handleStatusClickOutside);\n };\n }, [isStatusMenuOpen, statusMenuRef]);\n\n const onStatusToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (statusMenuRef.current) {\n const firstElement = statusMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsStatusMenuOpen(!isStatusMenuOpen);\n };\n\n function onStatusSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n setStatusSelection(itemId.toString());\n setIsStatusMenuOpen(!isStatusMenuOpen);\n }\n\n const statusToggle = (\n <MenuToggle\n ref={statusToggleRef}\n onClick={onStatusToggleClick}\n isExpanded={isStatusMenuOpen}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n Filter by status\n </MenuToggle>\n );\n\n const statusMenu = (\n <Menu ref={statusMenuRef} id=\"attribute-search-status-menu\" onSelect={onStatusSelect} selected={statusSelection}>\n <MenuContent>\n <MenuList>\n <MenuItem itemId=\"Degraded\">Degraded</MenuItem>\n <MenuItem itemId=\"Down\">Down</MenuItem>\n <MenuItem itemId=\"Needs maintenance\">Needs maintenance</MenuItem>\n <MenuItem itemId=\"Running\">Running</MenuItem>\n <MenuItem itemId=\"Stopped\">Stopped</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const statusSelect = (\n <div ref={statusContainerRef}>\n <Popper\n trigger={statusToggle}\n triggerRef={statusToggleRef}\n popper={statusMenu}\n popperRef={statusMenuRef}\n appendTo={statusContainerRef.current || undefined}\n isVisible={isStatusMenuOpen}\n />\n </div>\n );\n\n // Set up location checkbox select\n const [isLocationMenuOpen, setIsLocationMenuOpen] = useState<boolean>(false);\n const locationToggleRef = useRef<HTMLButtonElement>(null);\n const locationMenuRef = useRef<HTMLDivElement>(null);\n const locationContainerRef = useRef<HTMLDivElement>(null);\n\n const handleLocationMenuKeys = (event: KeyboardEvent) => {\n if (isLocationMenuOpen && locationMenuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsLocationMenuOpen(!isLocationMenuOpen);\n locationToggleRef.current?.focus();\n }\n }\n };\n\n const handleLocationClickOutside = (event: MouseEvent) => {\n if (isLocationMenuOpen && !locationMenuRef.current?.contains(event.target as Node)) {\n setIsLocationMenuOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleLocationMenuKeys);\n window.addEventListener('click', handleLocationClickOutside);\n return () => {\n window.removeEventListener('keydown', handleLocationMenuKeys);\n window.removeEventListener('click', handleLocationClickOutside);\n };\n }, [isLocationMenuOpen, locationMenuRef]);\n\n const onLocationMenuToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (locationMenuRef.current) {\n const firstElement = locationMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsLocationMenuOpen(!isLocationMenuOpen);\n };\n\n function onLocationMenuSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n const itemStr = itemId.toString();\n\n setLocationSelections(\n locationSelections.includes(itemStr)\n ? locationSelections.filter((selection) => selection !== itemStr)\n : [itemStr, ...locationSelections]\n );\n }\n\n const locationToggle = (\n <MenuToggle\n ref={locationToggleRef}\n onClick={onLocationMenuToggleClick}\n isExpanded={isLocationMenuOpen}\n {...(locationSelections.length > 0 && { badge: <Badge isRead>{locationSelections.length}</Badge> })}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n Filter by location\n </MenuToggle>\n );\n\n const locationMenu = (\n <Menu\n ref={locationMenuRef}\n id=\"attribute-search-location-menu\"\n onSelect={onLocationMenuSelect}\n selected={locationSelections}\n >\n <MenuContent>\n <MenuList>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Bangalore')} itemId=\"Bangalore\">\n Bangalore\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Boston')} itemId=\"Boston\">\n Boston\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Brno')} itemId=\"Brno\">\n Brno\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Raleigh')} itemId=\"Raleigh\">\n Raleigh\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Westford')} itemId=\"Westford\">\n Westford\n </MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const locationSelect = (\n <div ref={locationContainerRef}>\n <Popper\n trigger={locationToggle}\n triggerRef={locationToggleRef}\n popper={locationMenu}\n popperRef={locationMenuRef}\n appendTo={locationContainerRef.current || undefined}\n isVisible={isLocationMenuOpen}\n />\n </div>\n );\n\n // Set up attribute selector\n const [activeAttributeMenu, setActiveAttributeMenu] = useState<'Servers' | 'Status' | 'Location'>('Servers');\n const [isAttributeMenuOpen, setIsAttributeMenuOpen] = useState(false);\n const attributeToggleRef = useRef<HTMLButtonElement>(null);\n const attributeMenuRef = useRef<HTMLDivElement>(null);\n const attributeContainerRef = useRef<HTMLDivElement>(null);\n\n const handleAttribueMenuKeys = (event: KeyboardEvent) => {\n if (!isAttributeMenuOpen) {\n return;\n }\n if (\n attributeMenuRef.current?.contains(event.target as Node) ||\n attributeToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsAttributeMenuOpen(!isAttributeMenuOpen);\n attributeToggleRef.current?.focus();\n }\n }\n };\n\n const handleAttributeClickOutside = (event: MouseEvent) => {\n if (isAttributeMenuOpen && !attributeMenuRef.current?.contains(event.target as Node)) {\n setIsAttributeMenuOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleAttribueMenuKeys);\n window.addEventListener('click', handleAttributeClickOutside);\n return () => {\n window.removeEventListener('keydown', handleAttribueMenuKeys);\n window.removeEventListener('click', handleAttributeClickOutside);\n };\n }, [isAttributeMenuOpen, attributeMenuRef]);\n\n const onAttributeToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (attributeMenuRef.current) {\n const firstElement = attributeMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsAttributeMenuOpen(!isAttributeMenuOpen);\n };\n\n const attributeToggle = (\n <MenuToggle\n ref={attributeToggleRef}\n onClick={onAttributeToggleClick}\n isExpanded={isAttributeMenuOpen}\n icon={<FilterIcon />}\n >\n {activeAttributeMenu}\n </MenuToggle>\n );\n const attributeMenu = (\n <Menu\n ref={attributeMenuRef}\n onSelect={(_ev, itemId) => {\n setActiveAttributeMenu(itemId?.toString() as 'Servers' | 'Status' | 'Location');\n setIsAttributeMenuOpen(!isAttributeMenuOpen);\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId=\"Servers\">Servers</MenuItem>\n <MenuItem itemId=\"Status\">Status</MenuItem>\n <MenuItem itemId=\"Location\">Location</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const attributeDropdown = (\n <div ref={attributeContainerRef}>\n <Popper\n trigger={attributeToggle}\n triggerRef={attributeToggleRef}\n popper={attributeMenu}\n popperRef={attributeMenuRef}\n appendTo={attributeContainerRef.current || undefined}\n isVisible={isAttributeMenuOpen}\n />\n </div>\n );\n\n // Set up pagination and toolbar\n const toolbarPagination = (\n <Pagination\n titles={{ paginationAriaLabel: 'Attribute search pagination' }}\n itemCount={repositories.length}\n perPage={10}\n page={1}\n widgetId=\"attribute-search-mock-pagination\"\n isCompact\n />\n );\n\n const toolbar = (\n <Toolbar\n id=\"attribute-search-filter-toolbar\"\n clearAllFilters={() => {\n setSearchValue('');\n setStatusSelection('');\n setLocationSelections([]);\n }}\n >\n <ToolbarContent>\n <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>\n <ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint=\"xl\">\n <ToolbarGroup variant=\"filter-group\">\n <ToolbarItem>{attributeDropdown}</ToolbarItem>\n <ToolbarFilter\n labels={searchValue !== '' ? [searchValue] : ([] as string[])}\n deleteLabel={() => setSearchValue('')}\n deleteLabelGroup={() => setSearchValue('')}\n categoryName=\"Name\"\n showToolbarItem={activeAttributeMenu === 'Servers'}\n >\n {searchInput}\n </ToolbarFilter>\n <ToolbarFilter\n labels={statusSelection !== '' ? [statusSelection] : ([] as string[])}\n deleteLabel={() => setStatusSelection('')}\n deleteLabelGroup={() => setStatusSelection('')}\n categoryName=\"Status\"\n showToolbarItem={activeAttributeMenu === 'Status'}\n >\n {statusSelect}\n </ToolbarFilter>\n <ToolbarFilter\n labels={locationSelections}\n deleteLabel={(category, label) => onLocationMenuSelect(undefined, label as string)}\n deleteLabelGroup={() => setLocationSelections([])}\n categoryName=\"Location\"\n showToolbarItem={activeAttributeMenu === 'Location'}\n >\n {locationSelect}\n </ToolbarFilter>\n </ToolbarGroup>\n </ToolbarToggleGroup>\n <ToolbarItem variant=\"pagination\">{toolbarPagination}</ToolbarItem>\n </ToolbarContent>\n </Toolbar>\n );\n\n const emptyState = (\n <EmptyState headingLevel=\"h4\" titleText=\"No results found\" icon={SearchIcon}>\n <EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>\n <EmptyStateFooter>\n <EmptyStateActions>\n <Button\n variant=\"link\"\n onClick={() => {\n setSearchValue('');\n setStatusSelection('');\n setLocationSelections([]);\n }}\n >\n Clear all filters\n </Button>\n </EmptyStateActions>\n </EmptyStateFooter>\n </EmptyState>\n );\n\n return (\n <Fragment>\n {toolbar}\n <Table aria-label=\"Selectable table\">\n <Thead>\n <Tr>\n <Th screenReaderText=\"Row select\" />\n <Th width={20}>{columnNames.name}</Th>\n <Th width={10}>{columnNames.threads}</Th>\n <Th width={10}>{columnNames.apps}</Th>\n <Th width={10}>{columnNames.workspaces}</Th>\n <Th width={20}>{columnNames.status}</Th>\n <Th width={20}>{columnNames.location}</Th>\n </Tr>\n </Thead>\n <Tbody>\n {filteredRepos.length > 0 &&\n filteredRepos.map((repo, rowIndex) => (\n <Tr key={repo.name}>\n <Td\n select={{\n rowIndex,\n onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),\n isSelected: isRepoSelected(repo),\n isDisabled: !isRepoSelectable(repo)\n }}\n />\n <Td dataLabel={columnNames.name} modifier=\"truncate\">\n {repo.name}\n </Td>\n <Td dataLabel={columnNames.threads} modifier=\"truncate\">\n {repo.threads}\n </Td>\n <Td dataLabel={columnNames.apps} modifier=\"truncate\">\n {repo.apps}\n </Td>\n <Td dataLabel={columnNames.workspaces} modifier=\"truncate\">\n {repo.workspaces}\n </Td>\n <Td dataLabel={columnNames.status} modifier=\"truncate\">\n {repo.status}\n </Td>\n <Td dataLabel={columnNames.location} modifier=\"truncate\">\n {repo.location}\n </Td>\n </Tr>\n ))}\n {filteredRepos.length === 0 && (\n <Tr>\n <Td colSpan={8}>\n <Bullseye>{emptyState}</Bullseye>\n </Td>\n </Tr>\n )}\n </Tbody>\n </Table>\n </Fragment>\n );\n};\n","title":"Attribute search","lang":"ts","className":""}}>
|
|
108
|
+
|
|
109
|
+
</Example>,
|
|
110
|
+
'Mixed select filter group': props =>
|
|
111
|
+
<Example {...pageData} {...props} {...{"code":"import { Fragment, useEffect, useRef, useState } from 'react';\nimport {\n Toolbar,\n ToolbarContent,\n ToolbarItem,\n Menu,\n MenuContent,\n MenuList,\n MenuItem,\n MenuToggle,\n MenuToggleCheckbox,\n Popper,\n Pagination,\n EmptyState,\n EmptyStateFooter,\n EmptyStateBody,\n Button,\n Bullseye,\n Badge,\n ToolbarFilter,\n ToolbarToggleGroup,\n EmptyStateActions\n} from '@patternfly/react-core';\nimport { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';\nimport SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';\nimport FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';\n\ninterface Repository {\n name: string;\n threads: string;\n apps: string;\n workspaces: string;\n status: string;\n location: string;\n}\n\n// In real usage, this data would come from some external source like an API via props.\nconst repositories: Repository[] = [\n { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },\n { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },\n { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },\n { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },\n { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },\n { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },\n { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },\n { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },\n { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },\n { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }\n];\n\nconst columnNames = {\n name: 'Servers',\n threads: 'Threads',\n apps: 'Applications',\n workspaces: 'Workspaces',\n status: 'Status',\n location: 'Location'\n};\n\nexport const FilterMixedSelectGroup: React.FunctionComponent = () => {\n // Set up repo filtering\n const [locationSelections, setLocationSelections] = useState<string[]>([]);\n const [statusSelection, setStatusSelection] = useState('');\n\n const onFilter = (repo: Repository) => {\n // Search status with status selection\n const matchesStatusValue = repo.status.toLowerCase() === statusSelection.toLowerCase();\n\n // Search location with location selections\n const matchesLocationValue = locationSelections.includes(repo.location);\n\n return (statusSelection === '' || matchesStatusValue) && (locationSelections.length === 0 || matchesLocationValue);\n };\n const filteredRepos = repositories.filter(onFilter);\n\n // Set up table row selection\n // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.\n // This is to prevent state from being based on row order index in case we later add sorting.\n const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example\n const [selectedRepoNames, setSelectedRepoNames] = useState<string[]>([]);\n const setRepoSelected = (repo: Repository, isSelecting = true) =>\n setSelectedRepoNames((prevSelected) => {\n const otherSelectedRepoNames = prevSelected.filter((r) => r !== repo.name);\n return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;\n });\n const selectAllRepos = (isSelecting = true) =>\n setSelectedRepoNames(isSelecting ? filteredRepos.map((r) => r.name) : []); // Selecting all should only select all currently filtered rows\n const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;\n const areSomeReposSelected = selectedRepoNames.length > 0;\n const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);\n\n // To allow shift+click to select/deselect multiple rows\n const [recentSelectedRowIndex, setRecentSelectedRowIndex] = useState<number | null>(null);\n const [shifting, setShifting] = useState(false);\n\n const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {\n // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected\n if (shifting && recentSelectedRowIndex !== null) {\n const numberSelected = rowIndex - recentSelectedRowIndex;\n const intermediateIndexes =\n numberSelected > 0\n ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)\n : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);\n intermediateIndexes.forEach((index) => setRepoSelected(repositories[index], isSelecting));\n } else {\n setRepoSelected(repo, isSelecting);\n }\n setRecentSelectedRowIndex(rowIndex);\n };\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(true);\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(false);\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('keyup', onKeyUp);\n\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('keyup', onKeyUp);\n };\n }, []);\n\n // Set up bulk selection menu\n const bulkSelectMenuRef = useRef<HTMLDivElement>(null);\n const bulkSelectToggleRef = useRef<any>(null);\n const bulkSelectContainerRef = useRef<HTMLDivElement>(null);\n\n const [isBulkSelectOpen, setIsBulkSelectOpen] = useState<boolean>(false);\n\n const handleBulkSelectClickOutside = (event: MouseEvent) => {\n if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {\n setIsBulkSelectOpen(false);\n }\n };\n\n const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {\n if (!isBulkSelectOpen) {\n return;\n }\n if (\n bulkSelectMenuRef.current?.contains(event.target as Node) ||\n bulkSelectToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleBulkSelectMenuKeys);\n window.addEventListener('click', handleBulkSelectClickOutside);\n return () => {\n window.removeEventListener('keydown', handleBulkSelectMenuKeys);\n window.removeEventListener('click', handleBulkSelectClickOutside);\n };\n }, [isBulkSelectOpen, bulkSelectMenuRef]);\n\n const onBulkSelectToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (bulkSelectMenuRef.current) {\n const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n };\n\n let menuToggleCheckmark: boolean | null = false;\n if (areAllReposSelected) {\n menuToggleCheckmark = true;\n } else if (areSomeReposSelected) {\n menuToggleCheckmark = null;\n }\n\n const bulkSelectToggle = (\n <MenuToggle\n ref={bulkSelectToggleRef}\n onClick={onBulkSelectToggleClick}\n isExpanded={isBulkSelectOpen}\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"mixed-group-input-bulk-select\"\n key=\"mixed-group-input-bulk-select\"\n aria-label=\"Select all\"\n isChecked={menuToggleCheckmark}\n onChange={(checked, _event) => selectAllRepos(checked)}\n />\n ]}\n aria-label=\"Full table selection checkbox\"\n />\n );\n\n const bulkSelectMenu = (\n <Menu\n id=\"mixed-group-input-bulk-select\"\n ref={bulkSelectMenuRef}\n onSelect={(_ev, itemId) => {\n selectAllRepos(itemId === 1 || itemId === 2);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId={0}>Select none (0 items)</MenuItem>\n <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>\n <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const toolbarBulkSelect = (\n <div ref={bulkSelectContainerRef}>\n <Popper\n trigger={bulkSelectToggle}\n triggerRef={bulkSelectToggleRef}\n popper={bulkSelectMenu}\n popperRef={bulkSelectMenuRef}\n appendTo={bulkSelectContainerRef.current || undefined}\n isVisible={isBulkSelectOpen}\n />\n </div>\n );\n\n // Set up status single select\n const [isStatusMenuOpen, setIsStatusMenuOpen] = useState<boolean>(false);\n const statusToggleRef = useRef<HTMLButtonElement>(null);\n const statusMenuRef = useRef<HTMLDivElement>(null);\n const statusContainerRef = useRef<HTMLDivElement>(null);\n\n const handleStatusMenuKeys = (event: KeyboardEvent) => {\n if (isStatusMenuOpen && statusMenuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsStatusMenuOpen(!isStatusMenuOpen);\n statusToggleRef.current?.focus();\n }\n }\n };\n\n const handleStatusClickOutside = (event: MouseEvent) => {\n if (isStatusMenuOpen && !statusMenuRef.current?.contains(event.target as Node)) {\n setIsStatusMenuOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleStatusMenuKeys);\n window.addEventListener('click', handleStatusClickOutside);\n return () => {\n window.removeEventListener('keydown', handleStatusMenuKeys);\n window.removeEventListener('click', handleStatusClickOutside);\n };\n }, [isStatusMenuOpen, statusMenuRef]);\n\n const onStatusToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (statusMenuRef.current) {\n const firstElement = statusMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsStatusMenuOpen(!isStatusMenuOpen);\n };\n\n function onStatusSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n setStatusSelection(itemId.toString());\n setIsStatusMenuOpen(!isStatusMenuOpen);\n }\n\n const statusToggle = (\n <MenuToggle\n ref={statusToggleRef}\n onClick={onStatusToggleClick}\n isExpanded={isStatusMenuOpen}\n icon={<FilterIcon />}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n Status\n </MenuToggle>\n );\n\n const statusMenu = (\n <Menu ref={statusMenuRef} id=\"mixed-group-status-menu\" onSelect={onStatusSelect} selected={statusSelection}>\n <MenuContent>\n <MenuList>\n <MenuItem itemId=\"Degraded\">Degraded</MenuItem>\n <MenuItem itemId=\"Down\">Down</MenuItem>\n <MenuItem itemId=\"Needs maintenance\">Needs maintenance</MenuItem>\n <MenuItem itemId=\"Running\">Running</MenuItem>\n <MenuItem itemId=\"Stopped\">Stopped</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const statusSelect = (\n <div ref={statusContainerRef}>\n <Popper\n trigger={statusToggle}\n triggerRef={statusToggleRef}\n popper={statusMenu}\n popperRef={statusMenuRef}\n appendTo={statusContainerRef.current || undefined}\n isVisible={isStatusMenuOpen}\n />\n </div>\n );\n\n // Set up location checkbox select\n const [isLocationMenuOpen, setIsLocationMenuOpen] = useState<boolean>(false);\n const locationToggleRef = useRef<HTMLButtonElement>(null);\n const locationMenuRef = useRef<HTMLDivElement>(null);\n const locationContainerRef = useRef<HTMLDivElement>(null);\n\n const handleLocationMenuKeys = (event: KeyboardEvent) => {\n if (isLocationMenuOpen && locationMenuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsLocationMenuOpen(!isLocationMenuOpen);\n locationToggleRef.current?.focus();\n }\n }\n };\n\n const handleLocationClickOutside = (event: MouseEvent) => {\n if (isLocationMenuOpen && !locationMenuRef.current?.contains(event.target as Node)) {\n setIsLocationMenuOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleLocationMenuKeys);\n window.addEventListener('click', handleLocationClickOutside);\n return () => {\n window.removeEventListener('keydown', handleLocationMenuKeys);\n window.removeEventListener('click', handleLocationClickOutside);\n };\n }, [isLocationMenuOpen, locationMenuRef]);\n\n const onLocationMenuToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (locationMenuRef.current) {\n const firstElement = locationMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsLocationMenuOpen(!isLocationMenuOpen);\n };\n\n function onLocationMenuSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n const itemStr = itemId.toString();\n\n setLocationSelections(\n locationSelections.includes(itemStr)\n ? locationSelections.filter((selection) => selection !== itemStr)\n : [itemStr, ...locationSelections]\n );\n }\n\n const locationToggle = (\n <MenuToggle\n ref={locationToggleRef}\n onClick={onLocationMenuToggleClick}\n isExpanded={isLocationMenuOpen}\n {...(locationSelections.length > 0 && { badge: <Badge isRead>{locationSelections.length}</Badge> })}\n icon={<FilterIcon />}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n Location\n </MenuToggle>\n );\n\n const locationMenu = (\n <Menu\n ref={locationMenuRef}\n id=\"mixed-group-location-menu\"\n onSelect={onLocationMenuSelect}\n selected={locationSelections}\n >\n <MenuContent>\n <MenuList>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Bangalore')} itemId=\"Bangalore\">\n Bangalore\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Boston')} itemId=\"Boston\">\n Boston\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Brno')} itemId=\"Brno\">\n Brno\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Raleigh')} itemId=\"Raleigh\">\n Raleigh\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Westford')} itemId=\"Westford\">\n Westford\n </MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const locationSelect = (\n <div ref={locationContainerRef}>\n <Popper\n trigger={locationToggle}\n triggerRef={locationToggleRef}\n popper={locationMenu}\n popperRef={locationMenuRef}\n appendTo={locationContainerRef.current || undefined}\n isVisible={isLocationMenuOpen}\n />\n </div>\n );\n\n // Set up pagination and toolbar\n const toolbarPagination = (\n <Pagination\n titles={{ paginationAriaLabel: 'Mixed filter group pagination' }}\n itemCount={repositories.length}\n perPage={10}\n page={1}\n widgetId=\"mixed-group-mock-pagination\"\n isCompact\n />\n );\n\n const toolbar = (\n <Toolbar\n id=\"mixed-group-toolbar\"\n clearAllFilters={() => {\n setStatusSelection('');\n setLocationSelections([]);\n }}\n >\n <ToolbarContent>\n <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>\n <ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint=\"xl\">\n <ToolbarFilter\n labels={statusSelection !== '' ? [statusSelection] : ([] as string[])}\n deleteLabel={() => setStatusSelection('')}\n deleteLabelGroup={() => setStatusSelection('')}\n categoryName=\"Status\"\n >\n {statusSelect}\n </ToolbarFilter>\n <ToolbarFilter\n labels={locationSelections}\n deleteLabel={(category, label) => onLocationMenuSelect(undefined, label as string)}\n deleteLabelGroup={() => setLocationSelections([])}\n categoryName=\"Location\"\n >\n {locationSelect}\n </ToolbarFilter>\n </ToolbarToggleGroup>\n <ToolbarItem variant=\"pagination\">{toolbarPagination}</ToolbarItem>\n </ToolbarContent>\n </Toolbar>\n );\n\n const emptyState = (\n <EmptyState headingLevel=\"h4\" titleText=\"No results found\" icon={SearchIcon}>\n <EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>\n <EmptyStateFooter>\n <EmptyStateActions>\n <Button\n variant=\"link\"\n onClick={() => {\n setStatusSelection('');\n setLocationSelections([]);\n }}\n >\n Clear all filters\n </Button>\n </EmptyStateActions>\n </EmptyStateFooter>\n </EmptyState>\n );\n\n return (\n <Fragment>\n {toolbar}\n <Table aria-label=\"Selectable table\">\n <Thead>\n <Tr>\n <Th screenReaderText=\"Row select\" />\n <Th width={20}>{columnNames.name}</Th>\n <Th width={10}>{columnNames.threads}</Th>\n <Th width={10}>{columnNames.apps}</Th>\n <Th width={10}>{columnNames.workspaces}</Th>\n <Th width={20}>{columnNames.status}</Th>\n <Th width={20}>{columnNames.location}</Th>\n </Tr>\n </Thead>\n <Tbody>\n {filteredRepos.length > 0 &&\n filteredRepos.map((repo, rowIndex) => (\n <Tr key={repo.name}>\n <Td\n select={{\n rowIndex,\n onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),\n isSelected: isRepoSelected(repo),\n isDisabled: !isRepoSelectable(repo)\n }}\n />\n <Td dataLabel={columnNames.name} modifier=\"truncate\">\n {repo.name}\n </Td>\n <Td dataLabel={columnNames.threads} modifier=\"truncate\">\n {repo.threads}\n </Td>\n <Td dataLabel={columnNames.apps} modifier=\"truncate\">\n {repo.apps}\n </Td>\n <Td dataLabel={columnNames.workspaces} modifier=\"truncate\">\n {repo.workspaces}\n </Td>\n <Td dataLabel={columnNames.status} modifier=\"truncate\">\n {repo.status}\n </Td>\n <Td dataLabel={columnNames.location} modifier=\"truncate\">\n {repo.location}\n </Td>\n </Tr>\n ))}\n {filteredRepos.length === 0 && (\n <Tr>\n <Td colSpan={8}>\n <Bullseye>{emptyState}</Bullseye>\n </Td>\n </Tr>\n )}\n </Tbody>\n </Table>\n </Fragment>\n );\n};\n","title":"Mixed select filter group","lang":"ts","className":""}}>
|
|
112
|
+
|
|
113
|
+
</Example>,
|
|
114
|
+
'Single select filter group': props =>
|
|
115
|
+
<Example {...pageData} {...props} {...{"code":"import { Fragment, useEffect, useRef, useState } from 'react';\nimport {\n Toolbar,\n ToolbarContent,\n ToolbarItem,\n Menu,\n MenuContent,\n MenuList,\n MenuItem,\n MenuToggle,\n MenuToggleCheckbox,\n Popper,\n Pagination,\n EmptyState,\n EmptyStateFooter,\n EmptyStateBody,\n Button,\n Bullseye,\n ToolbarToggleGroup,\n EmptyStateActions\n} from '@patternfly/react-core';\nimport { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';\nimport SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';\nimport FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';\n\ninterface Repository {\n name: string;\n threads: string;\n apps: string;\n workspaces: string;\n status: string;\n location: string;\n}\n\n// In real usage, this data would come from some external source like an API via props.\nconst repositories: Repository[] = [\n { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },\n { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },\n { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },\n { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },\n { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },\n { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },\n { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },\n { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },\n { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },\n { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }\n];\n\nconst columnNames = {\n name: 'Servers',\n threads: 'Threads',\n apps: 'Applications',\n workspaces: 'Workspaces',\n status: 'Status',\n location: 'Location'\n};\n\nexport const FilterSameSelectGroup: React.FunctionComponent = () => {\n // Set up repo filtering\n const [locationSelection, setLocationSelection] = useState('All locations');\n const [statusSelection, setStatusSelection] = useState('All statuses');\n\n const onFilter = (repo: Repository) => {\n // Search status with status selection\n const matchesStatusValue = repo.status.toLowerCase() === statusSelection.toLowerCase();\n\n // Search location with location selections\n const matchesLocationValue = repo.location.toLowerCase() === locationSelection.toLowerCase();\n\n return (\n (statusSelection === 'All statuses' || matchesStatusValue) &&\n (locationSelection === 'All locations' || matchesLocationValue)\n );\n };\n const filteredRepos = repositories.filter(onFilter);\n\n // Set up table row selection\n // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.\n // This is to prevent state from being based on row order index in case we later add sorting.\n const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example\n const [selectedRepoNames, setSelectedRepoNames] = useState<string[]>([]);\n const setRepoSelected = (repo: Repository, isSelecting = true) =>\n setSelectedRepoNames((prevSelected) => {\n const otherSelectedRepoNames = prevSelected.filter((r) => r !== repo.name);\n return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;\n });\n const selectAllRepos = (isSelecting = true) =>\n setSelectedRepoNames(isSelecting ? filteredRepos.map((r) => r.name) : []); // Selecting all should only select all currently filtered rows\n const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;\n const areSomeReposSelected = selectedRepoNames.length > 0;\n const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);\n\n // To allow shift+click to select/deselect multiple rows\n const [recentSelectedRowIndex, setRecentSelectedRowIndex] = useState<number | null>(null);\n const [shifting, setShifting] = useState(false);\n\n const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {\n // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected\n if (shifting && recentSelectedRowIndex !== null) {\n const numberSelected = rowIndex - recentSelectedRowIndex;\n const intermediateIndexes =\n numberSelected > 0\n ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)\n : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);\n intermediateIndexes.forEach((index) => setRepoSelected(repositories[index], isSelecting));\n } else {\n setRepoSelected(repo, isSelecting);\n }\n setRecentSelectedRowIndex(rowIndex);\n };\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(true);\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(false);\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('keyup', onKeyUp);\n\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('keyup', onKeyUp);\n };\n }, []);\n\n // Set up bulk selection menu\n const bulkSelectMenuRef = useRef<HTMLDivElement>(null);\n const bulkSelectToggleRef = useRef<any>(null);\n const bulkSelectContainerRef = useRef<HTMLDivElement>(null);\n\n const [isBulkSelectOpen, setIsBulkSelectOpen] = useState<boolean>(false);\n\n const handleBulkSelectClickOutside = (event: MouseEvent) => {\n if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {\n setIsBulkSelectOpen(false);\n }\n };\n\n const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {\n if (!isBulkSelectOpen) {\n return;\n }\n if (\n bulkSelectMenuRef.current?.contains(event.target as Node) ||\n bulkSelectToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleBulkSelectMenuKeys);\n window.addEventListener('click', handleBulkSelectClickOutside);\n return () => {\n window.removeEventListener('keydown', handleBulkSelectMenuKeys);\n window.removeEventListener('click', handleBulkSelectClickOutside);\n };\n }, [isBulkSelectOpen, bulkSelectMenuRef]);\n\n const onBulkSelectToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (bulkSelectMenuRef.current) {\n const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n };\n\n let menuToggleCheckmark: boolean | null = false;\n if (areAllReposSelected) {\n menuToggleCheckmark = true;\n } else if (areSomeReposSelected) {\n menuToggleCheckmark = null;\n }\n\n const bulkSelectToggle = (\n <MenuToggle\n ref={bulkSelectToggleRef}\n onClick={onBulkSelectToggleClick}\n isExpanded={isBulkSelectOpen}\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"same-select-group-input-bulk-select\"\n key=\"same-select-group-input-bulk-select\"\n aria-label=\"Select all\"\n isChecked={menuToggleCheckmark}\n onChange={(checked, _event) => selectAllRepos(checked)}\n />\n ]}\n aria-label=\"Full table selection checkbox\"\n />\n );\n\n const bulkSelectMenu = (\n <Menu\n id=\"same-select-group-input-bulk-select\"\n ref={bulkSelectMenuRef}\n onSelect={(_ev, itemId) => {\n selectAllRepos(itemId === 1 || itemId === 2);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId={0}>Select none (0 items)</MenuItem>\n <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>\n <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const toolbarBulkSelect = (\n <div ref={bulkSelectContainerRef}>\n <Popper\n trigger={bulkSelectToggle}\n triggerRef={bulkSelectToggleRef}\n popper={bulkSelectMenu}\n popperRef={bulkSelectMenuRef}\n appendTo={bulkSelectContainerRef.current || undefined}\n isVisible={isBulkSelectOpen}\n />\n </div>\n );\n\n // Set up status single select\n const [isStatusMenuOpen, setIsStatusMenuOpen] = useState<boolean>(false);\n const statusToggleRef = useRef<HTMLButtonElement>(null);\n const statusMenuRef = useRef<HTMLDivElement>(null);\n const statusContainerRef = useRef<HTMLDivElement>(null);\n\n const handleStatusMenuKeys = (event: KeyboardEvent) => {\n if (isStatusMenuOpen && statusMenuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsStatusMenuOpen(!isStatusMenuOpen);\n statusToggleRef.current?.focus();\n }\n }\n };\n\n const handleStatusClickOutside = (event: MouseEvent) => {\n if (isStatusMenuOpen && !statusMenuRef.current?.contains(event.target as Node)) {\n setIsStatusMenuOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleStatusMenuKeys);\n window.addEventListener('click', handleStatusClickOutside);\n return () => {\n window.removeEventListener('keydown', handleStatusMenuKeys);\n window.removeEventListener('click', handleStatusClickOutside);\n };\n }, [isStatusMenuOpen, statusMenuRef]);\n\n const onStatusToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (statusMenuRef.current) {\n const firstElement = statusMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsStatusMenuOpen(!isStatusMenuOpen);\n };\n\n function onStatusSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n setStatusSelection(itemId.toString());\n setIsStatusMenuOpen(!isStatusMenuOpen);\n }\n\n const statusToggle = (\n <MenuToggle\n ref={statusToggleRef}\n onClick={onStatusToggleClick}\n isExpanded={isStatusMenuOpen}\n icon={<FilterIcon />}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n {statusSelection}\n </MenuToggle>\n );\n\n const statusMenu = (\n <Menu ref={statusMenuRef} id=\"same-select-group-status-menu\" onSelect={onStatusSelect} selected={statusSelection}>\n <MenuContent>\n <MenuList>\n <MenuItem itemId=\"All statuses\">All statuses</MenuItem>\n <MenuItem itemId=\"Degraded\">Degraded</MenuItem>\n <MenuItem itemId=\"Down\">Down</MenuItem>\n <MenuItem itemId=\"Needs maintenance\">Needs maintenance</MenuItem>\n <MenuItem itemId=\"Running\">Running</MenuItem>\n <MenuItem itemId=\"Stopped\">Stopped</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const statusSelect = (\n <div ref={statusContainerRef}>\n <Popper\n trigger={statusToggle}\n triggerRef={statusToggleRef}\n popper={statusMenu}\n popperRef={statusMenuRef}\n appendTo={statusContainerRef.current || undefined}\n isVisible={isStatusMenuOpen}\n />\n </div>\n );\n\n // Set up location checkbox select\n const [isLocationMenuOpen, setIsLocationMenuOpen] = useState<boolean>(false);\n const locationToggleRef = useRef<HTMLButtonElement>(null);\n const locationMenuRef = useRef<HTMLDivElement>(null);\n const locationContainerRef = useRef<HTMLDivElement>(null);\n\n const handleLocationMenuKeys = (event: KeyboardEvent) => {\n if (isLocationMenuOpen && locationMenuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsLocationMenuOpen(!isLocationMenuOpen);\n locationToggleRef.current?.focus();\n }\n }\n };\n\n const handleLocationClickOutside = (event: MouseEvent) => {\n if (isLocationMenuOpen && !locationMenuRef.current?.contains(event.target as Node)) {\n setIsLocationMenuOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleLocationMenuKeys);\n window.addEventListener('click', handleLocationClickOutside);\n return () => {\n window.removeEventListener('keydown', handleLocationMenuKeys);\n window.removeEventListener('click', handleLocationClickOutside);\n };\n }, [isLocationMenuOpen, locationMenuRef]);\n\n const onLocationMenuToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (locationMenuRef.current) {\n const firstElement = locationMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsLocationMenuOpen(!isLocationMenuOpen);\n };\n\n function onLocationMenuSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n setLocationSelection(itemId.toString());\n setIsLocationMenuOpen(!isLocationMenuOpen);\n }\n\n const locationToggle = (\n <MenuToggle\n ref={locationToggleRef}\n onClick={onLocationMenuToggleClick}\n isExpanded={isLocationMenuOpen}\n icon={<FilterIcon />}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n {locationSelection}\n </MenuToggle>\n );\n\n const locationMenu = (\n <Menu\n ref={locationMenuRef}\n id=\"same-select-group-location-menu\"\n onSelect={onLocationMenuSelect}\n selected={locationSelection}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId=\"All locations\">All locations</MenuItem>\n <MenuItem itemId=\"Bangalore\">Bangalore</MenuItem>\n <MenuItem itemId=\"Boston\">Boston</MenuItem>\n <MenuItem itemId=\"Brno\">Brno</MenuItem>\n <MenuItem itemId=\"Raleigh\">Raleigh</MenuItem>\n <MenuItem itemId=\"Westford\">Westford</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const locationSelect = (\n <div ref={locationContainerRef}>\n <Popper\n trigger={locationToggle}\n triggerRef={locationToggleRef}\n popper={locationMenu}\n popperRef={locationMenuRef}\n appendTo={locationContainerRef.current || undefined}\n isVisible={isLocationMenuOpen}\n />\n </div>\n );\n\n // Set up pagination and toolbar\n const toolbarPagination = (\n <Pagination\n titles={{ paginationAriaLabel: 'Same select group pagination' }}\n itemCount={repositories.length}\n perPage={10}\n page={1}\n widgetId=\"same-select-group-mock-pagination\"\n isCompact\n />\n );\n\n const toolbar = (\n <Toolbar\n id=\"same-select-group-toolbar\"\n clearAllFilters={() => {\n setStatusSelection('All statuses');\n setLocationSelection('All locations');\n }}\n >\n <ToolbarContent>\n <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>\n <ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint=\"xl\">\n <ToolbarItem>{statusSelect}</ToolbarItem>\n <ToolbarItem>{locationSelect}</ToolbarItem>\n </ToolbarToggleGroup>\n <ToolbarItem variant=\"pagination\">{toolbarPagination}</ToolbarItem>\n </ToolbarContent>\n </Toolbar>\n );\n\n const emptyState = (\n <EmptyState headingLevel=\"h4\" titleText=\"No results found\" icon={SearchIcon}>\n <EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>\n <EmptyStateFooter>\n <EmptyStateActions>\n <Button\n variant=\"link\"\n onClick={() => {\n setStatusSelection('All statuses');\n setLocationSelection('All locations');\n }}\n >\n Clear all filters\n </Button>\n </EmptyStateActions>\n </EmptyStateFooter>\n </EmptyState>\n );\n\n return (\n <Fragment>\n {toolbar}\n <Table aria-label=\"Selectable table\">\n <Thead>\n <Tr>\n <Th screenReaderText=\"Row select\" />\n <Th width={20}>{columnNames.name}</Th>\n <Th width={10}>{columnNames.threads}</Th>\n <Th width={10}>{columnNames.apps}</Th>\n <Th width={10}>{columnNames.workspaces}</Th>\n <Th width={20}>{columnNames.status}</Th>\n <Th width={20}>{columnNames.location}</Th>\n </Tr>\n </Thead>\n <Tbody>\n {filteredRepos.length > 0 &&\n filteredRepos.map((repo, rowIndex) => (\n <Tr key={repo.name}>\n <Td\n select={{\n rowIndex,\n onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),\n isSelected: isRepoSelected(repo),\n isDisabled: !isRepoSelectable(repo)\n }}\n />\n <Td dataLabel={columnNames.name} modifier=\"truncate\">\n {repo.name}\n </Td>\n <Td dataLabel={columnNames.threads} modifier=\"truncate\">\n {repo.threads}\n </Td>\n <Td dataLabel={columnNames.apps} modifier=\"truncate\">\n {repo.apps}\n </Td>\n <Td dataLabel={columnNames.workspaces} modifier=\"truncate\">\n {repo.workspaces}\n </Td>\n <Td dataLabel={columnNames.status} modifier=\"truncate\">\n {repo.status}\n </Td>\n <Td dataLabel={columnNames.location} modifier=\"truncate\">\n {repo.location}\n </Td>\n </Tr>\n ))}\n {filteredRepos.length === 0 && (\n <Tr>\n <Td colSpan={8}>\n <Bullseye>{emptyState}</Bullseye>\n </Td>\n </Tr>\n )}\n </Tbody>\n </Table>\n </Fragment>\n );\n};\n","title":"Single select filter group","lang":"ts","className":""}}>
|
|
116
|
+
|
|
117
|
+
</Example>,
|
|
118
|
+
'Faceted filter': props =>
|
|
119
|
+
<Example {...pageData} {...props} {...{"code":"import { Fragment, useEffect, useRef, useState } from 'react';\nimport {\n Toolbar,\n ToolbarContent,\n ToolbarItem,\n Menu,\n MenuContent,\n MenuList,\n MenuItem,\n MenuToggle,\n MenuToggleCheckbox,\n Popper,\n Pagination,\n EmptyState,\n EmptyStateFooter,\n EmptyStateBody,\n Button,\n Bullseye,\n Badge,\n ToolbarFilter,\n ToolbarToggleGroup,\n MenuGroup,\n EmptyStateActions\n} from '@patternfly/react-core';\nimport { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';\nimport SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';\nimport FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';\n\ninterface Repository {\n name: string;\n threads: string;\n apps: string;\n workspaces: string;\n status: string;\n location: string;\n}\n\n// In real usage, this data would come from some external source like an API via props.\nconst repositories: Repository[] = [\n { name: 'US-Node 1', threads: '5', apps: '25', workspaces: '5', status: 'Stopped', location: 'Raleigh' },\n { name: 'US-Node 2', threads: '5', apps: '30', workspaces: '2', status: 'Down', location: 'Westford' },\n { name: 'US-Node 3', threads: '13', apps: '35', workspaces: '12', status: 'Degraded', location: 'Boston' },\n { name: 'US-Node 4', threads: '2', apps: '5', workspaces: '18', status: 'Needs Maintenance', location: 'Raleigh' },\n { name: 'US-Node 5', threads: '7', apps: '30', workspaces: '5', status: 'Running', location: 'Boston' },\n { name: 'US-Node 6', threads: '5', apps: '20', workspaces: '15', status: 'Stopped', location: 'Raleigh' },\n { name: 'CZ-Node 1', threads: '12', apps: '48', workspaces: '13', status: 'Down', location: 'Brno' },\n { name: 'CZ-Node 2', threads: '3', apps: '8', workspaces: '20', status: 'Running', location: 'Brno' },\n { name: 'CZ-Remote-Node 1', threads: '1', apps: '15', workspaces: '20', status: 'Down', location: 'Brno' },\n { name: 'Bangalore-Node 1', threads: '1', apps: '20', workspaces: '20', status: 'Running', location: 'Bangalore' }\n];\n\nconst columnNames = {\n name: 'Servers',\n threads: 'Threads',\n apps: 'Applications',\n workspaces: 'Workspaces',\n status: 'Status',\n location: 'Location'\n};\n\nexport const FilterFaceted: React.FunctionComponent = () => {\n // Set up repo filtering\n const [locationSelections, setLocationSelections] = useState<string[]>([]);\n const [statusSelections, setStatusSelections] = useState<string[]>([]);\n\n const onFilter = (repo: Repository) => {\n // Search status with status selection\n const matchesStatusValue = statusSelections.includes(repo.status);\n\n // Search location with location selections\n const matchesLocationValue = locationSelections.includes(repo.location);\n\n return (\n (statusSelections.length === 0 || matchesStatusValue) && (locationSelections.length === 0 || matchesLocationValue)\n );\n };\n const filteredRepos = repositories.filter(onFilter);\n\n // Set up table row selection\n // In this example, selected rows are tracked by the repo names from each row. This could be any unique identifier.\n // This is to prevent state from being based on row order index in case we later add sorting.\n const isRepoSelectable = (repo: Repository) => repo.name !== 'a'; // Arbitrary logic for this example\n const [selectedRepoNames, setSelectedRepoNames] = useState<string[]>([]);\n const setRepoSelected = (repo: Repository, isSelecting = true) =>\n setSelectedRepoNames((prevSelected) => {\n const otherSelectedRepoNames = prevSelected.filter((r) => r !== repo.name);\n return isSelecting && isRepoSelectable(repo) ? [...otherSelectedRepoNames, repo.name] : otherSelectedRepoNames;\n });\n const selectAllRepos = (isSelecting = true) =>\n setSelectedRepoNames(isSelecting ? filteredRepos.map((r) => r.name) : []); // Selecting all should only select all currently filtered rows\n const areAllReposSelected = selectedRepoNames.length === filteredRepos.length && filteredRepos.length > 0;\n const areSomeReposSelected = selectedRepoNames.length > 0;\n const isRepoSelected = (repo: Repository) => selectedRepoNames.includes(repo.name);\n\n // To allow shift+click to select/deselect multiple rows\n const [recentSelectedRowIndex, setRecentSelectedRowIndex] = useState<number | null>(null);\n const [shifting, setShifting] = useState(false);\n\n const onSelectRepo = (repo: Repository, rowIndex: number, isSelecting: boolean) => {\n // If the user is shift + selecting the checkboxes, then all intermediate checkboxes should be selected\n if (shifting && recentSelectedRowIndex !== null) {\n const numberSelected = rowIndex - recentSelectedRowIndex;\n const intermediateIndexes =\n numberSelected > 0\n ? Array.from(new Array(numberSelected + 1), (_x, i) => i + recentSelectedRowIndex)\n : Array.from(new Array(Math.abs(numberSelected) + 1), (_x, i) => i + rowIndex);\n intermediateIndexes.forEach((index) => setRepoSelected(repositories[index], isSelecting));\n } else {\n setRepoSelected(repo, isSelecting);\n }\n setRecentSelectedRowIndex(rowIndex);\n };\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(true);\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (e.key === 'Shift') {\n setShifting(false);\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n document.addEventListener('keyup', onKeyUp);\n\n return () => {\n document.removeEventListener('keydown', onKeyDown);\n document.removeEventListener('keyup', onKeyUp);\n };\n }, []);\n\n // Set up bulk selection menu\n const bulkSelectMenuRef = useRef<HTMLDivElement>(null);\n const bulkSelectToggleRef = useRef<any>(null);\n const bulkSelectContainerRef = useRef<HTMLDivElement>(null);\n\n const [isBulkSelectOpen, setIsBulkSelectOpen] = useState<boolean>(false);\n\n const handleBulkSelectClickOutside = (event: MouseEvent) => {\n if (isBulkSelectOpen && !bulkSelectMenuRef.current?.contains(event.target as Node)) {\n setIsBulkSelectOpen(false);\n }\n };\n\n const handleBulkSelectMenuKeys = (event: KeyboardEvent) => {\n if (!isBulkSelectOpen) {\n return;\n }\n if (\n bulkSelectMenuRef.current?.contains(event.target as Node) ||\n bulkSelectToggleRef.current?.contains(event.target as Node)\n ) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleBulkSelectMenuKeys);\n window.addEventListener('click', handleBulkSelectClickOutside);\n return () => {\n window.removeEventListener('keydown', handleBulkSelectMenuKeys);\n window.removeEventListener('click', handleBulkSelectClickOutside);\n };\n }, [isBulkSelectOpen, bulkSelectMenuRef]);\n\n const onBulkSelectToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (bulkSelectMenuRef.current) {\n const firstElement = bulkSelectMenuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n };\n\n let menuToggleCheckmark: boolean | null = false;\n if (areAllReposSelected) {\n menuToggleCheckmark = true;\n } else if (areSomeReposSelected) {\n menuToggleCheckmark = null;\n }\n\n const bulkSelectToggle = (\n <MenuToggle\n ref={bulkSelectToggleRef}\n onClick={onBulkSelectToggleClick}\n isExpanded={isBulkSelectOpen}\n splitButtonItems={[\n <MenuToggleCheckbox\n id=\"filter-faceted-input-bulk-select\"\n key=\"filter-faceted-input-bulk-select\"\n aria-label=\"Select all\"\n isChecked={menuToggleCheckmark}\n onChange={(checked, _event) => selectAllRepos(checked)}\n />\n ]}\n aria-label=\"Full table selection checkbox\"\n />\n );\n\n const bulkSelectMenu = (\n <Menu\n id=\"filter-faceted-input-bulk-select\"\n ref={bulkSelectMenuRef}\n onSelect={(_ev, itemId) => {\n selectAllRepos(itemId === 1 || itemId === 2);\n setIsBulkSelectOpen(!isBulkSelectOpen);\n bulkSelectToggleRef.current?.querySelector('button').focus();\n }}\n >\n <MenuContent>\n <MenuList>\n <MenuItem itemId={0}>Select none (0 items)</MenuItem>\n <MenuItem itemId={1}>Select page ({repositories.length} items)</MenuItem>\n <MenuItem itemId={2}>Select all ({repositories.length} items)</MenuItem>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const toolbarBulkSelect = (\n <div ref={bulkSelectContainerRef}>\n <Popper\n trigger={bulkSelectToggle}\n triggerRef={bulkSelectToggleRef}\n popper={bulkSelectMenu}\n popperRef={bulkSelectMenuRef}\n appendTo={bulkSelectContainerRef.current || undefined}\n isVisible={isBulkSelectOpen}\n />\n </div>\n );\n\n // Set up location checkbox select\n const [isOpen, setIsOpen] = useState<boolean>(false);\n const toggleRef = useRef<HTMLButtonElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const handleKeys = (event: KeyboardEvent) => {\n if (isOpen && menuRef.current?.contains(event.target as Node)) {\n if (event.key === 'Escape' || event.key === 'Tab') {\n setIsOpen(!isOpen);\n toggleRef.current?.focus();\n }\n }\n };\n\n const handleClickOutside = (event: MouseEvent) => {\n if (isOpen && !menuRef.current?.contains(event.target as Node)) {\n setIsOpen(false);\n }\n };\n\n useEffect(() => {\n window.addEventListener('keydown', handleKeys);\n window.addEventListener('click', handleClickOutside);\n return () => {\n window.removeEventListener('keydown', handleKeys);\n window.removeEventListener('click', handleClickOutside);\n };\n }, [isOpen, menuRef]);\n\n const onToggleClick = (ev: React.MouseEvent) => {\n ev.stopPropagation(); // Stop handleClickOutside from handling\n setTimeout(() => {\n if (menuRef.current) {\n const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');\n firstElement && (firstElement as HTMLElement).focus();\n }\n }, 0);\n setIsOpen(!isOpen);\n };\n\n const onSelect = (event: React.MouseEvent | undefined, itemId: string | number | undefined) => {\n if (typeof itemId === 'undefined') {\n return;\n }\n\n const itemStr = itemId.toString();\n const category = ['Raleigh', 'Boston', 'Brno', 'Westford', 'Bangalore'].includes(itemStr) ? 'location' : 'status';\n\n if (category === 'status') {\n setStatusSelections(\n statusSelections.includes(itemStr)\n ? statusSelections.filter((selection) => selection !== itemStr)\n : [itemStr, ...statusSelections]\n );\n } else {\n setLocationSelections(\n locationSelections.includes(itemStr)\n ? locationSelections.filter((selection) => selection !== itemStr)\n : [itemStr, ...locationSelections]\n );\n }\n };\n\n const onLabelDelete = (category: string, label: string) => {\n if (category === 'status') {\n setStatusSelections(statusSelections.filter((selection) => selection !== label));\n } else {\n setLocationSelections(locationSelections.filter((selection) => selection !== label));\n }\n };\n\n const areSelectionsPresent = locationSelections.length > 0 || statusSelections.length > 0;\n\n const toggle = (\n <MenuToggle\n ref={toggleRef}\n onClick={onToggleClick}\n isExpanded={isOpen}\n {...(areSelectionsPresent && {\n badge: <Badge isRead>{locationSelections.length + statusSelections.length}</Badge>\n })}\n icon={<FilterIcon />}\n style={\n {\n width: '200px'\n } as React.CSSProperties\n }\n >\n Filter\n </MenuToggle>\n );\n\n const menu = (\n <Menu ref={menuRef} id=\"filter-faceted-location-menu\" onSelect={onSelect} selected={locationSelections}>\n <MenuContent>\n <MenuList>\n <MenuGroup label=\"Status\">\n <MenuItem hasCheckbox isSelected={statusSelections.includes('Degraded')} itemId=\"Degraded\">\n Degraded\n </MenuItem>\n <MenuItem hasCheckbox isSelected={statusSelections.includes('Down')} itemId=\"Down\">\n Down\n </MenuItem>\n <MenuItem\n hasCheckbox\n isSelected={statusSelections.includes('Needs Maintenance')}\n itemId=\"Needs Maintenance\"\n >\n Needs Maintenance\n </MenuItem>\n <MenuItem hasCheckbox isSelected={statusSelections.includes('Running')} itemId=\"Running\">\n Running\n </MenuItem>\n <MenuItem hasCheckbox isSelected={statusSelections.includes('Stopped')} itemId=\"Stopped\">\n Stopped\n </MenuItem>\n </MenuGroup>\n <MenuGroup label=\"Location\">\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Bangalore')} itemId=\"Bangalore\">\n Bangalore\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Boston')} itemId=\"Boston\">\n Boston\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Brno')} itemId=\"Brno\">\n Brno\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Raleigh')} itemId=\"Raleigh\">\n Raleigh\n </MenuItem>\n <MenuItem hasCheckbox isSelected={locationSelections.includes('Westford')} itemId=\"Westford\">\n Westford\n </MenuItem>\n </MenuGroup>\n </MenuList>\n </MenuContent>\n </Menu>\n );\n\n const select = (\n <div ref={containerRef}>\n <Popper\n trigger={toggle}\n triggerRef={toggleRef}\n popper={menu}\n popperRef={menuRef}\n appendTo={containerRef.current || undefined}\n isVisible={isOpen}\n />\n </div>\n );\n\n // Set up pagination and toolbar\n const toolbarPagination = (\n <Pagination\n titles={{ paginationAriaLabel: 'Faceted filter group pagination' }}\n itemCount={repositories.length}\n perPage={10}\n page={1}\n widgetId=\"filter-faceted-mock-pagination\"\n isCompact\n />\n );\n\n const toolbar = (\n <Toolbar\n id=\"filter-faceted-toolbar\"\n clearAllFilters={() => {\n setStatusSelections([]);\n setLocationSelections([]);\n }}\n >\n <ToolbarContent>\n <ToolbarItem>{toolbarBulkSelect}</ToolbarItem>\n <ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint=\"xl\">\n <ToolbarFilter\n labels={statusSelections}\n deleteLabel={(category, label) => onLabelDelete(category as string, label as string)}\n deleteLabelGroup={() => setStatusSelections([])}\n categoryName=\"Status\"\n showToolbarItem={false}\n >\n <div />\n </ToolbarFilter>\n <ToolbarFilter\n labels={locationSelections}\n deleteLabel={(category, label) => onLabelDelete(category as string, label as string)}\n deleteLabelGroup={() => setLocationSelections([])}\n categoryName=\"Location\"\n >\n {select}\n </ToolbarFilter>\n </ToolbarToggleGroup>\n <ToolbarItem variant=\"pagination\">{toolbarPagination}</ToolbarItem>\n </ToolbarContent>\n </Toolbar>\n );\n\n const emptyState = (\n <EmptyState headingLevel=\"h4\" titleText=\"No results found\" icon={SearchIcon}>\n <EmptyStateBody>No results match the filter criteria. Clear all filters and try again.</EmptyStateBody>\n <EmptyStateFooter>\n <EmptyStateActions>\n <Button\n variant=\"link\"\n onClick={() => {\n setStatusSelections([]);\n setLocationSelections([]);\n }}\n >\n Clear all filters\n </Button>\n </EmptyStateActions>\n </EmptyStateFooter>\n </EmptyState>\n );\n\n return (\n <Fragment>\n {toolbar}\n <Table aria-label=\"Selectable table\">\n <Thead>\n <Tr>\n <Th screenReaderText=\"Row select\" />\n <Th width={20}>{columnNames.name}</Th>\n <Th width={10}>{columnNames.threads}</Th>\n <Th width={10}>{columnNames.apps}</Th>\n <Th width={10}>{columnNames.workspaces}</Th>\n <Th width={20}>{columnNames.status}</Th>\n <Th width={20}>{columnNames.location}</Th>\n </Tr>\n </Thead>\n <Tbody>\n {filteredRepos.length > 0 &&\n filteredRepos.map((repo, rowIndex) => (\n <Tr key={repo.name}>\n <Td\n select={{\n rowIndex,\n onSelect: (_event, isSelecting) => onSelectRepo(repo, rowIndex, isSelecting),\n isSelected: isRepoSelected(repo),\n isDisabled: !isRepoSelectable(repo)\n }}\n />\n <Td dataLabel={columnNames.name} modifier=\"truncate\">\n {repo.name}\n </Td>\n <Td dataLabel={columnNames.threads} modifier=\"truncate\">\n {repo.threads}\n </Td>\n <Td dataLabel={columnNames.apps} modifier=\"truncate\">\n {repo.apps}\n </Td>\n <Td dataLabel={columnNames.workspaces} modifier=\"truncate\">\n {repo.workspaces}\n </Td>\n <Td dataLabel={columnNames.status} modifier=\"truncate\">\n {repo.status}\n </Td>\n <Td dataLabel={columnNames.location} modifier=\"truncate\">\n {repo.location}\n </Td>\n </Tr>\n ))}\n {filteredRepos.length === 0 && (\n <Tr>\n <Td colSpan={8}>\n <Bullseye>{emptyState}</Bullseye>\n </Td>\n </Tr>\n )}\n </Tbody>\n </Table>\n </Fragment>\n );\n};\n","title":"Faceted filter","lang":"ts","className":""}}>
|
|
120
|
+
|
|
121
|
+
</Example>
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const Component = () => (
|
|
125
|
+
<React.Fragment>
|
|
126
|
+
<AutoLinkHeader {...{"id":"filtering-demos","headingLevel":"h2","className":"ws-title ws-h2"}}>
|
|
127
|
+
{`Filtering demos`}
|
|
128
|
+
</AutoLinkHeader>
|
|
129
|
+
{React.createElement(pageData.examples["Search input"])}
|
|
130
|
+
{React.createElement(pageData.examples["Single select"])}
|
|
131
|
+
{React.createElement(pageData.examples["Checkbox select"])}
|
|
132
|
+
{React.createElement(pageData.examples["Attribute search"])}
|
|
133
|
+
{React.createElement(pageData.examples["Mixed select filter group"])}
|
|
134
|
+
{React.createElement(pageData.examples["Single select filter group"])}
|
|
135
|
+
{React.createElement(pageData.examples["Faceted filter"])}
|
|
136
|
+
</React.Fragment>
|
|
137
|
+
);
|
|
138
|
+
Component.displayName = 'PatternsFiltersReactDemosDocs';
|
|
139
|
+
Component.pageData = pageData;
|
|
140
|
+
|
|
141
|
+
export default Component;
|