-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add layout and orientation props to RAC ListBox #4669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
ac065e7
8bb8ed3
ece67f9
578fc86
aa11522
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,75 @@ | ||
| import {Collection, DropTarget, DropTargetDelegate, Node} from '@react-types/shared'; | ||
| import {Collection, Direction, DropTarget, DropTargetDelegate, Node, Orientation} from '@react-types/shared'; | ||
| import {RefObject} from 'react'; | ||
|
|
||
| interface ListDropTargetDelegateOptions { | ||
| /** | ||
| * Whether the items are arranged in a stack or grid. | ||
| * @default 'stack' | ||
| */ | ||
| layout?: 'stack' | 'grid', | ||
| /** | ||
| * The primary orientation of the items. Usually this is the | ||
| * direction that the collection scrolls. | ||
| * @default 'vertical' | ||
| */ | ||
| orientation?: Orientation, | ||
| /** | ||
| * The horizontal layout direction. | ||
| * @default 'ltr' | ||
| */ | ||
| direction?: Direction | ||
| } | ||
|
|
||
| // Terms used in the below code: | ||
| // * "Primary" – The main layout direction. For stacks, this is the direction | ||
snowystinger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // that the stack is arranged in (e.g. horizontal or vertical). | ||
| // For grids, this is the main scroll direction. | ||
| // * "Secondary" – The secondary layout direction. For stacks, there is no secondary | ||
| // layout direction. For grids, this is the opposite of the primary direction. | ||
| // * "Flow" – The flow direction of the items. For stacks, this is the the primary | ||
| // direction. For grids, it is the secondary direction. | ||
|
|
||
| export class ListDropTargetDelegate implements DropTargetDelegate { | ||
| private collection: Collection<Node<unknown>>; | ||
| private ref: RefObject<HTMLElement>; | ||
| private layout: 'stack' | 'grid'; | ||
snowystinger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| private orientation: Orientation; | ||
| private direction: Direction; | ||
|
|
||
| constructor(collection: Collection<Node<unknown>>, ref: RefObject<HTMLElement>) { | ||
| constructor(collection: Collection<Node<unknown>>, ref: RefObject<HTMLElement>, options?: ListDropTargetDelegateOptions) { | ||
| this.collection = collection; | ||
| this.ref = ref; | ||
| this.layout = options?.layout || 'stack'; | ||
| this.orientation = options?.orientation || 'vertical'; | ||
| this.direction = options?.direction || 'ltr'; | ||
| } | ||
|
|
||
| private getPrimaryStart(rect: DOMRect) { | ||
| return this.orientation === 'horizontal' ? rect.left : rect.top; | ||
| } | ||
|
|
||
| private getPrimaryEnd(rect: DOMRect) { | ||
| return this.orientation === 'horizontal' ? rect.right : rect.bottom; | ||
| } | ||
|
|
||
| private getSecondaryStart(rect: DOMRect) { | ||
| return this.orientation === 'horizontal' ? rect.top : rect.left; | ||
| } | ||
|
|
||
| private getSecondaryEnd(rect: DOMRect) { | ||
| return this.orientation === 'horizontal' ? rect.bottom : rect.right; | ||
| } | ||
|
|
||
| private getFlowStart(rect: DOMRect) { | ||
| return this.layout === 'stack' ? this.getPrimaryStart(rect) : this.getSecondaryStart(rect); | ||
| } | ||
|
|
||
| private getFlowEnd(rect: DOMRect) { | ||
| return this.layout === 'stack' ? this.getPrimaryEnd(rect) : this.getSecondaryEnd(rect); | ||
| } | ||
|
|
||
| private getFlowSize(rect: DOMRect) { | ||
| return this.getFlowEnd(rect) - this.getFlowStart(rect); | ||
| } | ||
|
|
||
| getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget { | ||
|
|
@@ -16,8 +78,15 @@ export class ListDropTargetDelegate implements DropTargetDelegate { | |
| } | ||
|
|
||
| let rect = this.ref.current.getBoundingClientRect(); | ||
| x += rect.x; | ||
| y += rect.y; | ||
| let primary = this.orientation === 'horizontal' ? x : y; | ||
| let secondary = this.orientation === 'horizontal' ? y : x; | ||
| primary += this.getPrimaryStart(rect); | ||
| secondary += this.getSecondaryStart(rect); | ||
|
|
||
| let flow = this.layout === 'stack' ? primary : secondary; | ||
| let isPrimaryRTL = this.orientation === 'horizontal' && this.direction === 'rtl'; | ||
| let isSecondaryRTL = this.layout === 'grid' && this.orientation === 'vertical' && this.direction === 'rtl'; | ||
| let isFlowRTL = this.layout === 'stack' ? isPrimaryRTL : isSecondaryRTL; | ||
|
|
||
| let elements = this.ref.current.querySelectorAll('[data-key]'); | ||
| let elementMap = new Map<string, HTMLElement>(); | ||
|
|
@@ -35,11 +104,22 @@ export class ListDropTargetDelegate implements DropTargetDelegate { | |
| let item = items[mid]; | ||
| let element = elementMap.get(String(item.key)); | ||
| let rect = element.getBoundingClientRect(); | ||
| let update = (isGreater: boolean) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mind adding a comment or two to this code block (aka the stuff within the chain of if statements)? At a glance I think this is narrowing down which item in the collection to consider the drop target with respect to LTR/RTL + the primary/secondary directions but I can't easily tell if the |
||
| if (isGreater) { | ||
| low = mid + 1; | ||
| } else { | ||
| high = mid; | ||
| } | ||
| }; | ||
|
|
||
| if (y < rect.top) { | ||
| high = mid; | ||
| } else if (y > rect.bottom) { | ||
| low = mid + 1; | ||
| if (primary < this.getPrimaryStart(rect)) { | ||
| update(isPrimaryRTL); | ||
| } else if (primary > this.getPrimaryEnd(rect)) { | ||
| update(!isPrimaryRTL); | ||
| } else if (secondary < this.getSecondaryStart(rect)) { | ||
| update(isSecondaryRTL); | ||
| } else if (secondary > this.getSecondaryEnd(rect)) { | ||
| update(!isSecondaryRTL); | ||
| } else { | ||
| let target: DropTarget = { | ||
| type: 'item', | ||
|
|
@@ -49,19 +129,19 @@ export class ListDropTargetDelegate implements DropTargetDelegate { | |
|
|
||
| if (isValidDropTarget(target)) { | ||
| // Otherwise, if dropping on the item is accepted, try the before/after positions if within 5px | ||
| // of the top or bottom of the item. | ||
| if (y <= rect.top + 5 && isValidDropTarget({...target, dropPosition: 'before'})) { | ||
| target.dropPosition = 'before'; | ||
| } else if (y >= rect.bottom - 5 && isValidDropTarget({...target, dropPosition: 'after'})) { | ||
| target.dropPosition = 'after'; | ||
| // of the start or end of the item. | ||
| if (flow <= this.getFlowStart(rect) + 5 && isValidDropTarget({...target, dropPosition: 'before'})) { | ||
| target.dropPosition = isFlowRTL ? 'after' : 'before'; | ||
| } else if (flow >= this.getFlowEnd(rect) - 5 && isValidDropTarget({...target, dropPosition: 'after'})) { | ||
| target.dropPosition = isFlowRTL ? 'before' : 'after'; | ||
| } | ||
| } else { | ||
| // If dropping on the item isn't accepted, try the target before or after depending on the y position. | ||
| let midY = rect.top + rect.height / 2; | ||
| if (y <= midY && isValidDropTarget({...target, dropPosition: 'before'})) { | ||
| target.dropPosition = 'before'; | ||
| } else if (y >= midY && isValidDropTarget({...target, dropPosition: 'after'})) { | ||
| target.dropPosition = 'after'; | ||
| // If dropping on the item isn't accepted, try the target before or after depending on the position. | ||
| let mid = this.getFlowStart(rect) + this.getFlowSize(rect) / 2; | ||
| if (flow <= mid && isValidDropTarget({...target, dropPosition: 'before'})) { | ||
| target.dropPosition = isFlowRTL ? 'after' : 'before'; | ||
| } else if (flow >= mid && isValidDropTarget({...target, dropPosition: 'after'})) { | ||
| target.dropPosition = isFlowRTL ? 'before' : 'after'; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -73,18 +153,18 @@ export class ListDropTargetDelegate implements DropTargetDelegate { | |
| let element = elementMap.get(String(item.key)); | ||
| rect = element.getBoundingClientRect(); | ||
|
|
||
| if (Math.abs(y - rect.top) < Math.abs(y - rect.bottom)) { | ||
| if (primary < this.getPrimaryStart(rect) || Math.abs(flow - this.getFlowStart(rect)) < Math.abs(flow - this.getFlowEnd(rect))) { | ||
| return { | ||
| type: 'item', | ||
| key: item.key, | ||
| dropPosition: 'before' | ||
| dropPosition: isFlowRTL ? 'after' : 'before' | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| type: 'item', | ||
| key: item.key, | ||
| dropPosition: 'after' | ||
| dropPosition: isFlowRTL ? 'before' : 'after' | ||
| }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for your question about if some of this should be split out into a separate GridKeyboardDelegate, IMO it is fine to stay in here since it isn't a true grid movement pattern if that makes sense. The contents of a
layout="grid"are "wrapping" in a specific direction rather than being truly laid out in a grid layout and thus left/right or up/down will move focus onto the next row/column depending on the orientation.