Skip to content

Commit

Permalink
Restructure SubMenu & Implement new Light-in-Light mode [LG-4236, LG-…
Browse files Browse the repository at this point in the history
…4060, LG-3263, LG-3168, LG-3190] (#2380)

* create Menu.styles

* installs descendants in menu

* extract useMenuHeight

* init descendants

* pass onItemFocus from provider

* abstract out useUpdatedChildren

* creates useHighlightReducer

* cleanup reducer

* skip disabled elements

* implement descendant in submenu

* Update yarn.lock

* rm focus-visible styles

we always want focus

* fix menu item list style

* fix ts errors

* rm deprecated hooks

* rm debug text

* restructure test suite

* Create blue-crews-hope.md

* Updates stories

* adds controlled story

* modernizes spec file

* Update Menu.stories.tsx

* Update SplitButton.spec.tsx

* update split button pkg.json

* Update yarn.lock

* Delete getNewIndex.ts

* add // prettier-ignore

* mv HighlightReducer

Update getUpdatedIndex.ts

* Update .gitignore

* creates AriaLabelPropsWithChildren type

* uses AriaLabelPropsWithChildren in InputOption

* Create InputOptionContent generated story

* InputOptionContent use tokens, extend className

* inputOptionThemeStyles use color tokens

* Update titleClassName

* create & use InputOptionContext

* refactor inputOptionStyles

* fix inputoption icon placement & sizing

* update icon hover styles

* Update Avatar props (#2352)

* avatar accepts null text

* update generated stories

* changeset

* Update spotty-ghosts-play.md

* add turbo to clean (#2361)

* pr

* Update .gitignore

* create Menu.styles

* installs descendants in menu

* extract useMenuHeight

* init descendants

* pass onItemFocus from provider

* abstract out useUpdatedChildren

* creates useHighlightReducer

* cleanup reducer

* skip disabled elements

* implement descendant in submenu

* Update yarn.lock

* rm focus-visible styles

we always want focus

* fix menu item list style

* fix ts errors

* rm deprecated hooks

* rm debug text

* restructure test suite

* Create blue-crews-hope.md

* Updates stories

* adds controlled story

* modernizes spec file

* Update Menu.stories.tsx

* adds preserveIconSpace. Update unique classnames

* create Menu.styles

* installs descendants in menu

* extract useMenuHeight

* init descendants

* pass onItemFocus from provider

* abstract out useUpdatedChildren

* creates useHighlightReducer

* cleanup reducer

* skip disabled elements

* implement descendant in submenu

* Update yarn.lock

* rm focus-visible styles

we always want focus

* fix menu item list style

* fix ts errors

* rm deprecated hooks

* rm debug text

* restructure test suite

* Create blue-crews-hope.md

* Updates stories

* adds controlled story

* modernizes spec file

* Update Menu.stories.tsx

* Update SplitButton.spec.tsx

* update split button pkg.json

* Update yarn.lock

* Delete getNewIndex.ts

* add // prettier-ignore

* mv HighlightReducer

Update getUpdatedIndex.ts

* update icon hover styles

* pr

* Update package.json

* mv content

* WIP: implement input option

* update component exports

* Create big-wasps-fix.md

* Create shaggy-cheetahs-ring.md

* Update big-wasps-fix.md

* implements preserveIconSpace

* Renames selected -> checked

* creates separate InputOptionContent.stories

* Update big-wasps-fix.md

* updates menu item stories

* Implement active & destructive styles, add stories

* wip dark in light mode

* update active wedge to border.primary

* create DarkInLightMode story

* include darkMode in InputOptionContext

* fix renderDarkMenu stories

* spread args into InitialOpen story

* rm old highlight reducer

* rm unused descendant vars

* rm checked styles

* Update big-wasps-fix.md

* Create clean-apricots-provide.md

* typo

* fix bad merge

* revert wedge color to blue.base

* revert icon height to default

* use disabled prop on `Description`

* add style changes to changeset

* updates text highlight color targeting

* revert implementing of Label component

* add description to highlight story

* Update MenuItem.styles.ts

* fix menu item tests

* Update InputOption.style.ts

* waitForTransition accepts null arg

* WIP

* add ref to descendant object

* add ref to descendant object

* rm controls from controlled story

* Creates `useTraceUpdate` hook

* create stale descendant test

* update spec & stories

* do not register descendent if it doesn't exist

* Adds getDescendants function

* add documentation for `getDescendants`

* update docs

* use getDescendants within Menu

* fix stories TS

* add popover as dev dep

* Update package.json

* mv test utils

* Update yarn.lock

* Update useControlledState.ts

* Create SubMenu.stories.tsx

* sub menu uses menu item. create useChildrenHeight

* adds keydown to close submenu

* Update Menu.spec.tsx

* add serve & watch scripts

* disable active styles when highlighted

* update changesets

* add tests for AriaLabelPropsWithChildren

* update documentation

* Update .gitignore

* Update README.md

* fix nits

* add example to useTraceUpdate

* rename var

* fix testing lib version

* PolyRef x null. PolyProps x PropsWithRef

* use latest CLI

* update Submenu types

* Update styles.ts

* clean up submenu tests

* InternalMenuItemContent - Prevents nested buttons

* lgids

* test to ensure no nested buttons

* Create slimy-walls-cry.md

* Update RecursiveRecord.types.ts

* scaffold light mode styles

* updates menu light-mode styling

* updates dark in light mode styles

* update submenu indent styles

* cleanup highlight styles

* fix initial open logic

* add destructive styles to dark-in-light

* rm size from SB

* Update Menu.stories.tsx

* adds transition handler tests in submenu

* cleanup tests

* Adds tests for more complex menu interactions

* ensure focus remains on a submenu after opening

* add internal flags to descendants utils

* add getByIndex/id to descendants pkg

* refactor Highlight reducer

* Update SubMenu.tsx

* pass getDescendants into highlight reducer

* Updates Descendant index properties

* handle TransitionExiting in submenu

* resolves submenu focus bugs

* Update yarn.lock

* rm comments

* fixes generated stories

---------

Co-authored-by: Shaneeza <[email protected]>
  • Loading branch information
TheSonOfThomp and shaneeza committed Jun 25, 2024
1 parent f799a44 commit 7b28d64
Show file tree
Hide file tree
Showing 56 changed files with 1,767 additions and 1,456 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-schools-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/lib': patch
---

Fixes `RecursiveRecord` type
5 changes: 5 additions & 0 deletions .changeset/rare-coins-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/descendants': patch
---

Updates Descendant index properties after inserting & removing to ensure the index of the Descendant object matches the index within the Descendants list
12 changes: 12 additions & 0 deletions .changeset/slimy-walls-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@leafygreen-ui/menu': minor
---

## Features
- Clicking a submenu item that _does not_ have a click handler or `href` will toggle the submenu
- When focused on a submenu item, pressing the left/right arrow keys will close/open the menu (respectively)

## Structural changes
- Updates Submenu component to use `InputOption`
- Moves the submenu toggle button to be a sibling of the `InputOption`
- this avoids any potential nesting of `button` elements
5 changes: 5 additions & 0 deletions .changeset/strange-schools-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/descendants': minor
---

Adds & exports `getDescendantById` & `getDescendantByIndex` utilities
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
"publish": "yarn changeset publish --public",
"slackbot": "lg slackbot release",
"start": "npx storybook dev -p 9001 --no-version-updates",
"serve": "npx http-server storybook-static -c5",
"test": "lg test",
"unlink": "lg unlink",
"validate": "lg validate"
"validate": "lg validate",
"watch": "npx nodemon --watch packages/ -e tsx,ts --exec 'yarn build-storybook --test'"
},
"devDependencies": {
"@actions/core": "^1.10.1",
Expand Down
12 changes: 8 additions & 4 deletions packages/descendants/src/DescendantsReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
findDescendantIndexWithId,
findDOMIndex,
insertDescendantAt,
refreshDescendantIndexes,
removeIndex,
} from './utils/';
import { Descendant, DescendantsList } from './Descendants.types';
Expand Down Expand Up @@ -65,7 +66,7 @@ export const descendantsReducer = <T extends HTMLElement>(
if (!isElementRegistered) {
// The element is not yet registered

// If there are no tracked descendants, then this element is at index 0,
// 2. If there are no tracked descendants, then this element is at index 0,
// Otherwise, check the array of tracked elements to find what index this element should be
const element = action.ref.current;
const index = findDOMIndex(element, currentState);
Expand All @@ -78,14 +79,15 @@ export const descendantsReducer = <T extends HTMLElement>(
index,
};

// Add the new descendant at the given index
// 3. Add the new descendant at the given index
const newDescendants = insertDescendantAt(
currentState,
thisDescendant,
index,
);

return newDescendants;
const indexedDescendants = refreshDescendantIndexes(newDescendants);
return indexedDescendants;
}

return currentState;
Expand Down Expand Up @@ -121,7 +123,9 @@ export const descendantsReducer = <T extends HTMLElement>(
if (registeredIndex >= 0) {
// If an element exists with the given id, remove it
const newDescendants = removeIndex(currentState, registeredIndex);
return newDescendants;
const indexedDescendants = refreshDescendantIndexes(newDescendants);

return indexedDescendants;
}

// no change
Expand Down
1 change: 1 addition & 0 deletions packages/descendants/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
export { useDescendant } from './useDescendant';
export { useDescendantsContext } from './useDescendantsContext';
export { useInitDescendants } from './useInitDescendants';
export { getDescendantById, getDescendantByIndex } from './utils';
1 change: 1 addition & 0 deletions packages/descendants/src/utils/findDOMIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { isElementPreceding } from './isElementPreceding';
* findDOMIndex(<C>, descendants) // 2 (since <B> is the closest _tracked_ DOM node preceding <C>)
* ```
*
* @internal
*/
export function findDOMIndex<T extends HTMLElement>(
element: T,
Expand Down
2 changes: 2 additions & 0 deletions packages/descendants/src/utils/findDescendantIndexWithId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { DescendantsList } from '../Descendants.types';
* @param descendants
* @param id
* @returns The index of a descendant with the given id
*
* @internal
*/
export function findDescendantIndexWithId(
descendants: DescendantsList,
Expand Down
21 changes: 21 additions & 0 deletions packages/descendants/src/utils/getDescendant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Descendant, DescendantsList } from '../Descendants.types';

/**
* Returns the Descendant with the provided `id`, or undefined
*/
export const getDescendantById = (
id: string,
descendants: DescendantsList,
): Descendant | undefined => {
return descendants.find(d => d.id === id);
};

/**
* Returns the Descendant at the provided `index`, or undefined
*/
export const getDescendantByIndex = (
index: number,
descendants: DescendantsList,
): Descendant | undefined => {
return descendants[index];
};
2 changes: 2 additions & 0 deletions packages/descendants/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { findDescendantIndexWithId } from './findDescendantIndexWithId';
export { findDOMIndex } from './findDOMIndex';
export { getDescendantById, getDescendantByIndex } from './getDescendant';
export { insertDescendantAt } from './insertDescendantAt';
export { isElementPreceding } from './isElementPreceding';
export { refreshDescendantIndexes } from './refreshDescendantIndexes';
export { removeIndex } from './removeIndex';
2 changes: 2 additions & 0 deletions packages/descendants/src/utils/insertDescendantAt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Descendant } from '../Descendants.types';
* @param item The item to insert into the array
* @param index The index to insert the item at
* @returns A copy of the array with the item inserted at the specified index
*
* @internal
*/
export function insertDescendantAt<T extends HTMLElement>(
array: Array<Descendant<T>>,
Expand Down
5 changes: 4 additions & 1 deletion packages/descendants/src/utils/isElementPreceding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* Returns whether ElementA precedes ElementAB in the DOM
*
* See: https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
*/ export function isElementPreceding(elemA: HTMLElement, elemB: HTMLElement) {
*
* @internal
*/
export function isElementPreceding(elemA: HTMLElement, elemB: HTMLElement) {
return Boolean(
elemB.compareDocumentPosition(elemA) & Node.DOCUMENT_POSITION_PRECEDING,
);
Expand Down
12 changes: 12 additions & 0 deletions packages/descendants/src/utils/refreshDescendantIndexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { DescendantsList } from '../Descendants.types';

/**
* Returns a new descendants list with updated indexes.
*
* Call this after inserting/removing from the descendants list
*/
export const refreshDescendantIndexes = <T extends HTMLElement>(
descendants: DescendantsList<T>,
): DescendantsList<T> => {
return descendants.map((d, i) => ({ ...d, index: i }));
};
6 changes: 6 additions & 0 deletions packages/descendants/src/utils/removeIndex.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* Removes the given index from an array
*
* @internal
*/
// TODO: Move to `lib`
export function removeIndex<T extends any>(
array: Array<T>,
index: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface InputOptionContentProps extends ComponentProps<'div'> {
rightGlyph?: React.ReactNode;

/**
*
* Preserves space before the text content for a left glyph.
*
* Use in menus where some items may or may not have icons/glyphs,
Expand Down
21 changes: 13 additions & 8 deletions packages/lib/src/types/RecursiveRecord.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@
export type RecursiveRecord<
Keys extends Array<any>,
Strict extends boolean = true,
> = Keys extends [
infer Key, // the current union of keys
...infer Rest,
]
? Strict extends true
? Record<Key & string, RecursiveRecord<Rest, Strict>>
: Partial<Record<Key & string, RecursiveRecord<Rest, Strict>>>
: Keys;
> =
// If `Keys` is an array with at least 2 indexes
Keys extends [
infer Key, // the current union of keys
...infer Rest extends [infer _K, ...infer _R], // (`Keys` has at least 2 indexes if 2nd argument can also be inferred)
]
? // If this is strict, then don't use Partial
Strict extends true
? Record<Key & string, RecursiveRecord<Rest, Strict>>
: Partial<Record<Key & string, RecursiveRecord<Rest, Strict>>>
: Keys extends [infer Key] // If Keys has only 1 index
? Key // return that index
: never; // otherwise there's an error
1 change: 1 addition & 0 deletions packages/menu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@leafygreen-ui/polymorphic": "^2.0.0",
"@leafygreen-ui/tokens": "^2.9.0",
"lodash": "^4.17.21",
"polished": "^4.3.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
Expand Down
109 changes: 109 additions & 0 deletions packages/menu/src/HighlightReducer/HighlightReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { type Reducer, useReducer } from 'react';

import {
Descendant,
DescendantsList,
getDescendantById,
getDescendantByIndex,
} from '@leafygreen-ui/descendants';

import { getNextFromDirection } from './utils/getNextFromDirection';
import { isDescendantsSet } from './utils/isDescendantsSet';
import type {
Direction,
HighlightChangeHandler,
HighlightReducerFunction,
HighlightReducerReturnType,
UpdateHighlightAction,
} from './highlight.types';

const getInitialHighlight = (descendants: DescendantsList<HTMLElement>) =>
isDescendantsSet(descendants) ? descendants[0] : undefined;

/**
* Creates a new reducer function for closure for a given `descendants` value
*/
const makeHighlightReducerFunction =
(getDescendants: () => DescendantsList): HighlightReducerFunction =>
(currentHighlight, action) => {
const descendants = getDescendants();

// If we've received a direction, move the highlight
if (action.direction) {
const nextHighlight = getNextFromDirection(
action.direction,
currentHighlight,
descendants,
);
return nextHighlight || currentHighlight;
} else if (action.index) {
const nextHighlight = getDescendantByIndex(action.index, descendants);
return nextHighlight;
} else if (action.id) {
const nextHighlight = getDescendantById(action.id, descendants);
return nextHighlight;
}

return currentHighlight;
};

/**
* Custom hook that handles setting the highlighted descendant index,
* and fires any `onChange` side effects
*/
export const useHighlightReducer = (
/** An accessor for the updated descendants list */
getDescendants: () => DescendantsList,
/** A callback fired when the highlight changes */
onChange?: HighlightChangeHandler,
): HighlightReducerReturnType => {
// Create a reducer function
const highlightReducerFunction = makeHighlightReducerFunction(getDescendants);

// Create the reducer
const [highlight, dispatch] = useReducer<
Reducer<Descendant | undefined, UpdateHighlightAction>
>(highlightReducerFunction, getInitialHighlight(getDescendants()));

/**
* Custom dispatch that moves the current highlight
* in a given direction
*
* Fires any side-effects in the `onChange` callback
*/
const moveHighlight = (direction: Direction) => {
const updatedHighlight = highlightReducerFunction(highlight, {
direction,
});

onChange?.(updatedHighlight);
dispatch({ direction });
};

/**
* Custom dispatch that sets the current highlight
* to a given `index` or `id`.
*
* Fires any side-effects in the `onChange` callback
*/
const setHighlight = (indexOrId: number | string) => {
const action =
typeof indexOrId === 'string'
? {
id: indexOrId,
}
: {
index: indexOrId,
};

const updatedHighlight = highlightReducerFunction(highlight, action);
onChange?.(updatedHighlight);
dispatch(action);
};

return {
highlight,
moveHighlight,
setHighlight,
};
};
44 changes: 44 additions & 0 deletions packages/menu/src/HighlightReducer/highlight.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Reducer } from 'react';

import { Descendant } from '@leafygreen-ui/descendants';

export type Index = number | undefined;
const Direction = {
Next: 'next',
Prev: 'prev',
First: 'first',
Last: 'last',
} as const;
export type Direction = (typeof Direction)[keyof typeof Direction];

export type HighlightChangeHandler = (
nextHighlight: Descendant | undefined,
) => void;

export type UpdateHighlightAction =
| {
direction: Direction;
index?: never;
id?: never;
}
| {
index: number;
direction?: never;
id?: never;
}
| {
id: string;
direction?: never;
index?: never;
};

export type HighlightReducerFunction = Reducer<
Descendant | undefined,
UpdateHighlightAction
>;

export interface HighlightReducerReturnType {
highlight: Descendant | undefined;
moveHighlight: (direction: Direction) => void;
setHighlight: (indexOrId: number | string) => void;
}
2 changes: 2 additions & 0 deletions packages/menu/src/HighlightReducer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { Direction, Index } from './highlight.types';
export { useHighlightReducer } from './HighlightReducer';
Loading

0 comments on commit 7b28d64

Please sign in to comment.