diff --git a/packages/gamut/src/List/List.tsx b/packages/gamut/src/List/List.tsx index fd29ccd97b3..f4afee3ebd6 100644 --- a/packages/gamut/src/List/List.tsx +++ b/packages/gamut/src/List/List.tsx @@ -1,7 +1,7 @@ import { DotLoose } from '@codecademy/gamut-patterns'; import { timingValues } from '@codecademy/gamut-styles'; import isArray from 'lodash/isArray'; -import { ComponentProps, forwardRef, useEffect, useRef, useState } from 'react'; +import { ComponentProps, forwardRef, useEffect } from 'react'; import * as React from 'react'; import { Box, BoxProps, FlexBox } from '../Box'; @@ -12,6 +12,7 @@ import { shadowVariant, StaticListWrapper, } from './elements'; +import { useScrollabilityCheck } from './hooks'; import { ListProvider, useList } from './ListProvider'; import { AllListProps } from './types'; @@ -72,11 +73,6 @@ export const List = forwardRef( const isEmpty = !children || (isArray(children) && children.length === 0); const isTable = as === 'table'; - const [isEnd, setIsEnd] = useState(false); - const ListWrapper = shadow ? AnimatedListWrapper : StaticListWrapper; - const showShadow = shadow && scrollable && !isEnd && !isEmpty; - const animationVar = showShadow ? 'shadow' : 'hidden'; - const value = useList({ listType: as, rowBreakpoint, @@ -85,16 +81,12 @@ export const List = forwardRef( variant, }); - const wrapperRef = useRef(null); - const tableRef = useRef(null); - - useEffect(() => { - const wrapperWidth = - wrapperRef?.current?.getBoundingClientRect()?.width ?? 0; - const tableWidth = tableRef?.current?.getBoundingClientRect().width ?? 0; + const { isEnd, tableRef, setWrapperRef, handleScroll } = + useScrollabilityCheck({ shadow, scrollable, children, loading, isEmpty }); - setIsEnd(tableWidth < wrapperWidth); - }, []); + const ListWrapper = shadow ? AnimatedListWrapper : StaticListWrapper; + const showShadow = shadow && scrollable && !isEnd && !isEmpty; + const animationVar = showShadow ? 'shadow' : 'hidden'; useEffect(() => { if (scrollToTopOnUpdate && tableRef.current !== null) { @@ -113,11 +105,6 @@ export const List = forwardRef( ); - const scrollHandler = (event: React.UIEvent) => { - const { offsetWidth, scrollLeft, scrollWidth } = event.currentTarget; - setIsEnd(offsetWidth + Math.ceil(scrollLeft) >= scrollWidth); - }; - const listContents = ( <> {header} @@ -153,7 +140,7 @@ export const List = forwardRef( maxHeight={height} overflow={overflow} position="relative" - ref={wrapperRef} + ref={setWrapperRef} transition={{ background: { duration: timingValues.fast, ease: 'easeInOut' }, }} @@ -162,7 +149,7 @@ export const List = forwardRef( shadow: shadowVariant, }} width={1} - onScroll={scrollable ? scrollHandler : undefined} + onScroll={scrollable ? handleScroll : undefined} > { + const { offsetWidth, scrollLeft, scrollWidth } = wrapper; + const isAtHorizontalEnd = offsetWidth + Math.ceil(scrollLeft) >= scrollWidth; + const hasNoHorizontalScroll = scrollWidth <= offsetWidth; + + return hasNoHorizontalScroll || isAtHorizontalEnd; +}; + +type ScrollabilityCheckParams = Pick< + ListProps, + 'shadow' | 'scrollable' | 'children' | 'loading' +> & { + isEmpty: boolean; +}; + +/** + * Custom hook to manage scrollability state for shadow indicators. + * Only runs when shadow and scrollable props are enabled for performance. + * + * @param params - Parameters from ListProps needed for scrollability checking + * @returns Object containing isEnd state and refs for wrapper and table elements + */ +export const useScrollabilityCheck = ({ + shadow = false, + scrollable = false, + children, + loading, + isEmpty, +}: ScrollabilityCheckParams) => { + const wrapperRef = useRef(null); + const tableRef = useRef(null); + + const needsScrollabilityCheck = shadow && scrollable; + + const [isEnd, setIsEnd] = useState(true); + + const checkScrollability = useCallback(() => { + if (!needsScrollabilityCheck) { + return; + } + + const wrapper = wrapperRef?.current; + + if (!wrapper) { + setIsEnd(false); + return; + } + + setIsEnd(checkIsAtScrollEnd(wrapper)); + }, [needsScrollabilityCheck]); + + // Helper to check scrollability after next frame (for DOM readiness) + const checkScrollAvailable = useCallback(() => { + requestAnimationFrame(() => { + checkScrollability(); + }); + }, [checkScrollability]); + + const setWrapperRef = useCallback( + (node: HTMLDivElement | null) => { + (wrapperRef as React.MutableRefObject).current = + node; + if (node && needsScrollabilityCheck) { + checkScrollAvailable(); + } + }, + [needsScrollabilityCheck, checkScrollAvailable] + ); + + const handleResize = useCallback(() => { + checkScrollability(); + }, [checkScrollability]); + + const resizeObserverCallback = useCallback(() => { + checkScrollAvailable(); + }, [checkScrollAvailable]); + + useEffect(() => { + if (!needsScrollabilityCheck) { + return; + } + + checkScrollAvailable(); + + window.addEventListener('resize', handleResize, { passive: true }); + + let resizeObserver: ResizeObserver | null = null; + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(resizeObserverCallback); + + if (wrapperRef.current) { + resizeObserver.observe(wrapperRef.current); + } + if (tableRef.current) { + resizeObserver.observe(tableRef.current); + } + } + + return () => { + window.removeEventListener('resize', handleResize); + if (resizeObserver) { + resizeObserver.disconnect(); + } + }; + }, [ + children, + loading, + isEmpty, + needsScrollabilityCheck, + checkScrollability, + handleResize, + resizeObserverCallback, + checkScrollAvailable, + ]); + + const handleScroll = useCallback( + (event: React.UIEvent) => { + if (!needsScrollabilityCheck) { + return; + } + const { offsetWidth, scrollLeft, scrollWidth } = event.currentTarget; + setIsEnd(offsetWidth + Math.ceil(scrollLeft) >= scrollWidth); + }, + [needsScrollabilityCheck] + ); + + return { + isEnd, + tableRef, + setWrapperRef, + handleScroll, + }; +}; diff --git a/packages/gamut/src/Modals/Dialog.tsx b/packages/gamut/src/Modals/Dialog.tsx index 1d67b046e53..7ad3acd7d09 100644 --- a/packages/gamut/src/Modals/Dialog.tsx +++ b/packages/gamut/src/Modals/Dialog.tsx @@ -25,11 +25,6 @@ export interface DialogProps extends ModalBaseProps, CloseButtonProps { >; confirmCta: DialogButtonProps; cancelCta?: DialogButtonProps; - /** - * TEMPORARY: a stopgap solution to avoid zIndex conflicts - - * will be reworked with: GM-624 - */ - zIndex?: number; } export const Dialog: React.FC = ({ diff --git a/packages/gamut/src/Modals/Modal.tsx b/packages/gamut/src/Modals/Modal.tsx index 90c9bbdd511..dc3db860b29 100644 --- a/packages/gamut/src/Modals/Modal.tsx +++ b/packages/gamut/src/Modals/Modal.tsx @@ -71,11 +71,6 @@ export interface MultiViewModalProps * Optional array of multiple screens */ views: ModalViewProps[]; - /** - * TEMPORARY: a stopgap solution to avoid zIndex conflicts - - * will be reworked with: GM-624 - */ - zIndex?: number; } export type ModalProps = SingleViewModalProps | MultiViewModalProps; diff --git a/packages/gamut/src/Modals/types.ts b/packages/gamut/src/Modals/types.ts index fd9185cdc34..a7c5ed4b2f0 100644 --- a/packages/gamut/src/Modals/types.ts +++ b/packages/gamut/src/Modals/types.ts @@ -13,6 +13,7 @@ export interface ModalOverlayProps | 'clickOutsideCloses' | 'escapeCloses' | 'shroud' + | 'zIndex' > {} export interface ModalBaseProps diff --git a/packages/gamut/src/Overlay/index.tsx b/packages/gamut/src/Overlay/index.tsx index 6a22232b216..9ddda4854d0 100644 --- a/packages/gamut/src/Overlay/index.tsx +++ b/packages/gamut/src/Overlay/index.tsx @@ -34,8 +34,8 @@ export type OverlayProps = { /** Whether the overlay allows scroll */ allowScroll?: boolean; /** - * TEMPORARY: a stopgap solution to avoid zIndex conflicts - - * will be reworked with: GM-624 + * z-index for the Overlay. Defaults to 3 to appear above common UI elements + * like headers . Can be overridden when needed for custom stacking orders. */ zIndex?: number; }; @@ -61,7 +61,7 @@ export const Overlay: React.FC = ({ onRequestClose, isOpen, allowScroll = false, - zIndex = 1, + zIndex = 3, }) => { const handleOutsideClick = useCallback(() => { if (clickOutsideCloses) { diff --git a/packages/gamut/src/PopoverContainer/PopoverContainer.tsx b/packages/gamut/src/PopoverContainer/PopoverContainer.tsx index 4e13b36763c..6f938d86e01 100644 --- a/packages/gamut/src/PopoverContainer/PopoverContainer.tsx +++ b/packages/gamut/src/PopoverContainer/PopoverContainer.tsx @@ -13,7 +13,7 @@ import { BodyPortal } from '../BodyPortal'; import { FocusTrap } from '../FocusTrap'; import { useResizingParentEffect, useScrollingParentsEffect } from './hooks'; import { ContainerState, PopoverContainerProps } from './types'; -import { getContainers, getPosition, isInView } from './utils'; +import { getContainers, getPosition, isOutOfView } from './utils'; const PopoverContent = styled.div( variance.compose( @@ -37,6 +37,7 @@ export const PopoverContainer: React.FC = ({ onRequestClose, targetRef, allowPageInteraction, + closeOnViewportExit = false, ...rest }) => { const popoverRef = useRef(null); @@ -98,17 +99,22 @@ export const PopoverContainer: React.FC = ({ useResizingParentEffect(targetRef, setTargetRect); useIsomorphicLayoutEffect(() => { + if (!closeOnViewportExit) return; + if ( containers?.viewport && - !isInView(containers?.viewport) && + isOutOfView(containers?.viewport, targetRef?.current as HTMLElement) && !hasRequestedCloseRef.current ) { hasRequestedCloseRef.current = true; onRequestClose?.(); - } else if (containers?.viewport && isInView(containers?.viewport)) { + } else if ( + containers?.viewport && + !isOutOfView(containers?.viewport, targetRef?.current as HTMLElement) + ) { hasRequestedCloseRef.current = false; } - }, [containers?.viewport, onRequestClose]); + }, [containers?.viewport, onRequestClose, targetRef, closeOnViewportExit]); /** * Allows targetRef to be or contain a button that toggles the popover open and closed. diff --git a/packages/gamut/src/PopoverContainer/__tests__/PopoverContainer.test.tsx b/packages/gamut/src/PopoverContainer/__tests__/PopoverContainer.test.tsx index 63dc9778d08..6a18ecf8d0c 100644 --- a/packages/gamut/src/PopoverContainer/__tests__/PopoverContainer.test.tsx +++ b/packages/gamut/src/PopoverContainer/__tests__/PopoverContainer.test.tsx @@ -120,21 +120,22 @@ describe('Popover', () => { expect(onRequestClose).toBeCalledTimes(1); }); - it('triggers onRequestClose callback when popover is out of viewport', () => { - /* element is inside the viewport if the top and left value is greater than or equal to 0, - and right value is less than or equal to window.innerWidth - and bottom value is less than or equal to window.innerHeight */ - const targetRefObj = mockTargetRef({}, { top: -1, x: 41, y: -1 }); + it('triggers onRequestClose callback when popover is out of viewport and closeOnViewportExit is true', () => { + const targetRefObj = mockTargetRef( + {}, + { top: -201, bottom: -1, left: 150, right: 350, x: 150, y: -201 } + ); const onRequestClose = jest.fn(); renderView({ targetRef: targetRefObj, onRequestClose, + closeOnViewportExit: true, }); expect(onRequestClose).toBeCalledTimes(1); }); - it('does not onRequestClose callback when popover is out of viewport', () => { + it('does not trigger onRequestClose callback when popover is in viewport', () => { const targetRefObj = mockTargetRef({}, { top: 1, x: 41, y: 1 }); const onRequestClose = jest.fn(); @@ -145,6 +146,35 @@ describe('Popover', () => { expect(onRequestClose).toBeCalledTimes(0); }); + it('does not trigger onRequestClose callback when closeOnViewportExit is false (default)', () => { + const targetRefObj = mockTargetRef( + {}, + { top: -201, bottom: -1, left: 150, right: 350, x: 150, y: -201 } + ); + + const onRequestClose = jest.fn(); + renderView({ + targetRef: targetRefObj, + onRequestClose, + }); + expect(onRequestClose).toBeCalledTimes(0); + }); + + it('triggers onRequestClose callback when closeOnViewportExit is true', () => { + const targetRefObj = mockTargetRef( + {}, + { top: -201, bottom: -1, left: 150, right: 350, x: 150, y: -201 } + ); + + const onRequestClose = jest.fn(); + renderView({ + targetRef: targetRefObj, + onRequestClose, + closeOnViewportExit: true, + }); + expect(onRequestClose).toBeCalledTimes(1); + }); + describe('alignments', () => { describe('render context', () => { describe('portal - viewport', () => { diff --git a/packages/gamut/src/PopoverContainer/types.ts b/packages/gamut/src/PopoverContainer/types.ts index afbd3d61b87..2c14f860579 100644 --- a/packages/gamut/src/PopoverContainer/types.ts +++ b/packages/gamut/src/PopoverContainer/types.ts @@ -87,4 +87,9 @@ export interface PopoverContainerProps * If true, it will allow outside page interaction. Popover container will still close when clicking outside of the popover or hitting the escape key. */ allowPageInteraction?: boolean; + /** + * If true, the popover will automatically close when the target element moves out of viewport. + * Defaults to false. + */ + closeOnViewportExit?: boolean; } diff --git a/packages/gamut/src/PopoverContainer/utils.ts b/packages/gamut/src/PopoverContainer/utils.ts index 815009573e5..5a40b7ef2dd 100644 --- a/packages/gamut/src/PopoverContainer/utils.ts +++ b/packages/gamut/src/PopoverContainer/utils.ts @@ -43,14 +43,42 @@ export const findAllAdditionalScrollingParents = ( return scrollingParents; }; -export const isInView = ({ top, left, bottom, right }: DOMRect) => { +export const isOutOfView = ( + rect: DOMRect, + target?: HTMLElement | null +): boolean => { const windowHeight = window.innerHeight || document.documentElement.clientHeight; const windowWidth = window.innerWidth || document.documentElement.clientWidth; - return ( - top >= 0 && left >= 0 && bottom <= windowHeight && right <= windowWidth - ); + const outOfViewport = + rect.bottom < 0 || + rect.top > windowHeight || + rect.right < 0 || + rect.left > windowWidth; + + if (outOfViewport || !target) { + return outOfViewport; + } + + const scrollingParents = findAllAdditionalScrollingParents(target); + + for (const parent of scrollingParents) { + const parentRect = parent.getBoundingClientRect(); + + const intersects = + rect.top < parentRect.bottom && + rect.bottom > parentRect.top && + rect.left < parentRect.right && + rect.right > parentRect.left; + + // If element doesn't intersect with a scrollable parent's visible area, it's out of view + if (!intersects) { + return true; + } + } + + return false; }; export const ALIGN = { diff --git a/packages/styleguide/src/lib/Atoms/PopoverContainer/PopoverContainer.mdx b/packages/styleguide/src/lib/Atoms/PopoverContainer/PopoverContainer.mdx index e291186c6fb..1e798427472 100644 --- a/packages/styleguide/src/lib/Atoms/PopoverContainer/PopoverContainer.mdx +++ b/packages/styleguide/src/lib/Atoms/PopoverContainer/PopoverContainer.mdx @@ -83,6 +83,35 @@ Absolute offset values irrespective of the relative position. (e.g. x={-4} will When `allowPageInteraction` is set to `true`, the page interaction is allowed. This is useful when the parent component is handling focus management and accessibility. +### `closeOnViewportExit` + +By default, `closeOnViewportExit` is set to `false`, which means the popover will remain open even if the target element moves out of the viewport. This is the recommended default for interactive menus (like filter menus or action menus) where users need to interact with the popover content. + +If you set `closeOnViewportExit` to `true`, the popover will automatically close when the target element moves completely out of viewport. This can be useful for: + +- Informational popovers that should disappear when the target is no longer visible +- Preventing orphaned popovers when content scrolls away +- Notifications or temporary UI elements that should clean up automatically + +```tsx +// Default behavior - popover stays open even if target scrolls out of view + + + Filter Option 1 + Filter Option 2 + + + +// Explicitly close when target exits viewport + + Some helpful information + +``` + ## Playground diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx index 4809aa0373b..e19cf0d54ff 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.mdx @@ -163,6 +163,122 @@ const MyDarkDataTable = () => { +### Floating menus with PopoverContainer + +You can add interactive menus to DataTable rows using `PopoverContainer` and `Menu` components. This is useful for row-specific actions like edit, delete, or view details. + + + +**Key features:** + +- **Automatic closing**: The PopoverContainer has the `closeOnViewportExit` prop which automatically closes when the Menu when the target row scrolls out of view, even when inside scrollable parent containers +- **Integration with Modals**: You can open Modals from menu items, which will appear above the table header with proper z-index stacking +- **Accessible**: The menu is keyboard accessible and works with screen readers + +**Example implementation:** + +```tsx +import { + Box, + DataTable, + IconButton, + Menu, + MenuItem, + Modal, + PopoverContainer, + Text, +} from '@codecademy/gamut'; +import { MiniKebabMenuIcon } from '@codecademy/gamut-icons'; +import { useRef, useState } from 'react'; + +const RowMenu: React.FC<{ rowName: string }> = ({ rowName }) => { + const [isOpen, setIsOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const menuButtonRef = useRef(null); + + const handleClose = () => { + setIsOpen(false); + }; + + const handleOpenModal = () => { + setIsOpen(false); + setIsModalOpen(true); + }; + + return ( + + setIsOpen(!isOpen)} + /> + + + + + + Edit {rowName} + + + + + View Details + + + + + Delete {rowName} + + + + + + setIsModalOpen(false)} + > + Modal content for {rowName} + + + ); +}; + +// Use in DataTable columns +const columns = [ + // ... other columns + { + header: 'Actions', + key: 'name', + size: 'sm', + justify: 'right', + type: 'control', + render: (row) => , + }, +]; +``` + +**Important considerations:** + +- Use a `ref` for the menu trigger element (e.g., IconButton) and pass it to `PopoverContainer`'s `targetRef` prop +- Set `allowPageInteraction={true}` on PopoverContainer to allow interaction with the table while the menu is open +- The PopoverContainer will automatically close when the target row scrolls out of view, preventing orphaned menus +- Modals opened from menu items will use `zIndex={3}` by default to appear above table headers + ## Container Query Control DataTable inherits container query functionality from the underlying List component. By default, DataTable uses CSS container queries to determine responsive behavior based on the container width rather than viewport width. This ensures optimal display when DataTable is placed in constrained layouts. diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.stories.tsx index 04859220abe..14bb8f5f508 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataTable/DataTable.stories.tsx @@ -1,9 +1,20 @@ // Added because SB and TS don't play nice with each other at the moment // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import { DataTable } from '@codecademy/gamut'; +import { + Box, + DataTable, + Dialog, + IconButton, + Menu, + MenuItem, + PopoverContainer, + Text, +} from '@codecademy/gamut'; +import { MiniKebabMenuIcon } from '@codecademy/gamut-icons'; import { Background } from '@codecademy/gamut-styles'; import type { Meta, StoryObj } from '@storybook/react'; +import { useRef, useState } from 'react'; import { CustomEmptyState, @@ -210,3 +221,261 @@ export const DisableContainerQuery: Story = { args: {}, render: () => , }; + +const RowMenu: React.FC<{ rowName: string }> = ({ rowName }) => { + const [isOpen, setIsOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const menuButtonRef = useRef(null); + + const handleClose = () => { + setIsOpen(false); + }; + + const handleOpenModal = () => { + setIsOpen(false); + setIsModalOpen(true); + }; + + return ( + + setIsOpen(!isOpen)} + /> + + + + + + Edit {rowName} + + + + + Delete {rowName} + + + + + Clone {rowName} + + + + + + setIsModalOpen(false), + }} + confirmCta={{ + children: 'Clone', + onClick: () => { + // Handle clone action here + setIsModalOpen(false); + }, + }} + isOpen={isModalOpen} + size="small" + title={`Clone ${rowName}`} + onRequestClose={() => setIsModalOpen(false)} + > + Are you sure you want to clone{' '} + + {rowName}{' '} + + ? This action cannot be undone. + + + ); +}; + +export const WithFloatingMenu: Story = { + args: { + id: 'crew-with-menu', + idKey: 'name', + query: { sort: { name: 'desc', role: 'asc' } }, + rows: [ + { + name: 'Jean Luc Picard', + 'a very important role': 'Captain', + ship: 'USS Enterprise', + age: '59', + species: 'Human', + sector: 'Alpha Quadrant', + status: 'Active', + yearsOfService: '35', + homeworld: 'La Barre, France, Earth', + specialization: 'Command & Diplomacy', + }, + { + name: 'Wesley Crusher', + 'a very important role': 'Deus Ex Machina', + ship: 'USS Enterprise', + age: '18', + species: 'Human/Traveler', + sector: 'Multiple Dimensions', + status: 'Transcended', + yearsOfService: '2', + homeworld: 'Earth', + specialization: 'Space-Time Manipulation', + }, + { + name: 'Geordie LaForge', + 'a very important role': 'Chief Engineer / Rascal', + ship: 'Borg Cube', + age: '35', + species: 'Human', + sector: 'Alpha Quadrant', + status: 'Active', + yearsOfService: '15', + homeworld: 'Mogadishu, Somalia, Earth', + specialization: 'Engineering & Technology', + }, + { + name: 'Data', + 'a very important role': 'Lt. Commander / Scamp', + ship: 'He is a ship', + age: '30', + species: 'Soong-type Android', + sector: 'Alpha Quadrant', + status: 'Active', + yearsOfService: '26', + homeworld: 'Omicron Theta', + specialization: 'Operations & Analysis', + }, + { + name: `Miles Edward O'Brien, 24th Century Man`, + 'a very important role': 'Command Master Chief', + ship: 'Deep Space 9', + age: '40', + species: 'Human', + sector: 'Bajoran System', + status: 'Active', + yearsOfService: '22', + homeworld: 'Ireland, Earth', + specialization: 'Engineering & Transporter Operations', + }, + { + name: 'William Riker', + 'a very important role': 'Commander', + ship: 'USS Enterprise', + age: '32', + species: 'Human', + sector: 'Alpha Quadrant', + status: 'Active', + yearsOfService: '15', + homeworld: 'Alaska, Earth', + specialization: 'Command & Diplomacy', + }, + { + name: 'Deanna Troi', + 'a very important role': 'Counselor', + ship: 'USS Enterprise', + age: '28', + species: 'Human / Betazoid', + sector: 'Alpha Quadrant', + status: 'Active', + yearsOfService: '8', + homeworld: 'Betazed', + specialization: 'Psychology & Empathy', + }, + { + name: 'Worf', + 'a very important role': 'Security Officer', + ship: 'USS Enterprise', + age: '35', + species: 'Klingon', + sector: 'Alpha Quadrant', + status: 'Active', + yearsOfService: '12', + homeworld: "Qo'noS", + specialization: 'Security & Combat', + }, + { + name: 'Beverly Crusher', + 'a very important role': 'Chief Medical Officer', + ship: 'USS Enterprise', + age: '40', + species: 'Human', + sector: 'Alpha Quadrant', + status: 'Active', + yearsOfService: '18', + homeworld: 'Earth', + specialization: 'Medicine & Research', + }, + { + name: 'Tasha Yar', + 'a very important role': 'Security Chief', + ship: 'USS Enterprise', + age: '24', + species: 'Human', + sector: 'Alpha Quadrant', + status: 'Deceased', + yearsOfService: '3', + homeworld: 'Turkana IV, Earth', + specialization: 'Security & Tactics', + }, + ], + columns: [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + sortable: true, + }, + { + header: 'Rank', + key: 'a very important role', + size: 'lg', + sortable: true, + }, + { header: 'Ship', key: 'ship', size: 'lg', sortable: true }, + { header: 'Age', key: 'age', size: 'sm', sortable: true }, + { header: 'Species', key: 'species', size: 'md', sortable: true }, + { header: 'Sector', key: 'sector', size: 'md', sortable: true }, + { header: 'Status', key: 'status', size: 'sm', sortable: true }, + { + header: 'Years of Service', + key: 'yearsOfService', + size: 'sm', + sortable: true, + }, + { header: 'Homeworld', key: 'homeworld', size: 'lg', sortable: true }, + { + header: 'Specialization', + key: 'specialization', + size: 'xl', + sortable: true, + fill: true, + }, + { + header: 'Actions', + key: 'name', + size: 'sm', + justify: 'right', + type: 'control', + render: (row) => , + }, + ], + spacing: 'condensed', + shadow: true, + scrollable: true, + height: '400px', + }, +}; diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/examples.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/examples.tsx index b2fbe8b31c4..4a9b19219db 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/examples.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/examples.tsx @@ -100,6 +100,7 @@ const CrewMgmtDropdown: React.FC<{ , }, - { - header: 'Controls', - key: 'name', - size: 'md', - justify: 'right', - type: 'control', - render: (row) => , - }, ] as ColumnConfig<(typeof crew)[number]>[]; export const createDemoTable =