From 5a5c21afc3e107e97273b2e6316b579ac203d30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 1 Mar 2023 14:06:13 +0100 Subject: [PATCH 01/49] Few fixes and additions to useListbox --- docs/pages/experiments/base/listbox.tsx | 220 ++++++++++++++++++ .../MenuUnstyled/useMenuChangeNotifiers.ts | 2 +- .../useListbox/defaultListboxReducer.test.ts | 19 ++ .../src/useListbox/defaultListboxReducer.ts | 68 +++++- packages/mui-base/src/useListbox/index.ts | 1 + .../src/useListbox/useControllableReducer.ts | 3 +- .../src/useListbox/useListChangeNotifiers.ts | 68 ++++++ .../mui-base/src/useListbox/useListbox.ts | 195 +++++++++++++++- .../src/useListbox/useListbox.types.ts | 14 ++ .../src/useSelect/useSelectChangeNotifiers.ts | 4 +- packages/mui-base/src/utils/useMessageBus.ts | 10 +- 11 files changed, 576 insertions(+), 28 deletions(-) create mode 100644 docs/pages/experiments/base/listbox.tsx create mode 100644 packages/mui-base/src/useListbox/useListChangeNotifiers.ts diff --git a/docs/pages/experiments/base/listbox.tsx b/docs/pages/experiments/base/listbox.tsx new file mode 100644 index 00000000000000..1ea7e10fbd0374 --- /dev/null +++ b/docs/pages/experiments/base/listbox.tsx @@ -0,0 +1,220 @@ +/* eslint-disable react/no-danger */ +import * as React from 'react'; +import clsx from 'clsx'; +import useListbox, { ListContext, useListItem } from '@mui/base/useListbox'; + +const styles = ` + body { + padding: 0; + margin: 0; + font-family: IBM Plex Sans, sans-serif; + } + + .list { + display: flex; + gap: 16px; + align-items: center; + justify-content: flex-start; + max-height: 70vh; + padding: 16px; + width: 100vw; + flex-wrap: wrap; + background: linear-gradient(-30deg, #009245, #FCEE21); + box-sizing: border-box; + margin: 0; + } + + .list:focus-visible { + outline: none; + } + + .item { + width: 50px; + height: 50px; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + box-sizing: border-box; + background: linear-gradient(-30deg, #f5f5f5, #fff); + font-size: 18px; + color: #333; + user-select: none; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1), 0 4px 4px -2px rgba(0, 0, 0, 0.15); + } + + .item.highlighted:not([tabindex]), + .item:focus-visible + { + outline: 2px solid #fff; + outline-offset: 4px; + } + + .item.selected { + background: linear-gradient(-30deg, #363636, #666); + color: #fff; + } + + .controls { + padding: 16px; + } + + .controls > div { + margin-bottom: 8px; + } + + .controls button { + border: 1px solid #999; + background: #fff; + padding: 8px 16px; + border-radius: 4px; + font-family: inherit; + margin: 0 4px; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1), 0 4px 4px -2px rgba(0, 0, 0, 0.15); + } + + .controls button.active { + background: #73bc34; + color: #fff; + } + + .state { + padding: 16px; + background: #202020; + color: #fff; + }`; + +const items = Array.from({ length: 200 }, (_, i) => i + 1); + +const Item = React.forwardRef(function Item( + props: React.PropsWithChildren<{ value: number }>, + ref: React.Ref, +) { + const item = props.value; + const { getItemProps, getItemState } = useListItem({ item, ref }); + + const itemProps = getItemProps(); + const state = getItemState(); + + const classes = clsx('item', { + highlighted: state.highlighted, + selected: state.selected, + }); + + return ( +
+ {item} +
+ ); +}); + +function List() { + const [orientation, setOrientation] = React.useState< + 'horizontal-ltr' | 'horizontal-rtl' | 'vertical' + >('horizontal-ltr'); + const [focusManagement, setFocusManagement] = React.useState<'activeDescendant' | 'DOM'>( + 'activeDescendant', + ); + + let flexDirection: React.CSSProperties['flexDirection']; + switch (orientation) { + case 'horizontal-ltr': + flexDirection = 'row'; + break; + case 'horizontal-rtl': + flexDirection = 'row-reverse'; + break; + default: + flexDirection = 'column'; + break; + } + + const itemRefs = React.useRef(new Map()); + const listbox = useListbox({ + options: items, + getOptionElement: (item) => itemRefs.current.get(item) || null, + orientation, + focusManagement, + selectionLimit: 1, + }); + + const { getRootProps, contextValue, selectedOptions, highlightedOption } = listbox; + + const handleItemRef = React.useCallback((item: number, node: HTMLElement | null) => { + if (node) { + itemRefs.current.set(item, node); + } else { + itemRefs.current.delete(item); + } + }, []); + + return ( + +
+
+ Orientation:  + + + +
+
+ Focus management:  + + +
+
+
+
Selected: {selectedOptions.join(', ')}
+
Highlighted: {highlightedOption ?? '(none)'}
+
+
+ + {items.map((item) => ( + handleItemRef(item, node)} /> + ))} + +
+
+ ); +} + +export default function Demo() { + return ( + +