-
Notifications
You must be signed in to change notification settings - Fork 124
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
Table view 543 (Milestone 1) #2764
base: main
Are you sure you want to change the base?
Conversation
This PR is ready for review. |
…ider same-level siblings when it's the only child, handling Table View and non-Table View scenarios.
Both of the two latest commits successfully resolve the current issue. In the most recent commit, I've implemented state-based logic using selectors wherever possible instead of relying solely on DOM queries. However, since we still need to use DOM queries to measure text widths, does it make sense to combine state-based logic with selectors and DOM queries in this way? Or would it be better to use only DOM queries, as in the previous commit? |
…c using selectors.
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.
Thank you for the PR, @ohrytskov!
This seems to work when pasting the given examples from the issue:
1. Divider widths do not update
The divider widths do not update while editing the thoughts.
Screen.Recording.2025-01-15.at.4.39.21.AM.mov
Including children and thoughtsAtSameDepth in the useLayoutEffect dependency array caused an infinite re-render loop because these arrays were recreated with new references on each render. Since the hook's dependency array relies on referential equality, and the arrays were derived from selectors that created new instances of the Redux state values, the effect ran repeatedly. This triggered state updates and re-renders in a loop. To resolve this, the arrays were memoized with useMemo, ensuring their references only changed when their contents did. |
@trevinhofmann, I've made the following updates:
|
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.
Thank you, @ohrytskov! The latest changes resolved the issues 4. Divider widths are longer than thoughts
and 5. Divider widths affected by selected thought
.
I am seeing one other issue now.
6. Divider continues to grow after text wraps
When a thought's text wraps to a new line, the divider continues to grow wider. The divider should reach a maximum width.
Screen.Recording.2025-01-28.at.7.03.39.PM.mov
@trevinhofmann, the PR is ready for the next review. Let me know if any further changes are needed. |
…ents and using DOM getBoundingClientRect instead.
The new solution now uses DOM elements to measure the maximum width of thoughts instead of relying on canvas text measurement. Since accounting for text wrapping requires accessing the actual element, we no longer need canvas for width calculations.This also simplifies the logic by returning to DOM-based measurements, making it more reliable for real-time UI updates. |
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.
Thank you, @ohrytskov!
The previous issues are all fixed, and the functionality on desktop and mobile appears to be working as expected.
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.
Thank! I really like the new approach. It is lightweight since it avoids ResizeObserver and Canvas, and almost all of the logic is contained within the Divider component. Nice work.
There are a couple more dependencies that need to trigger updateDividerWidth
for it to be updated when the font size changes and the window is resized:
const fontSize = useSelector(state => state.fontSize)
viewportStore.useSelectorEffect(updateDividerWidth, state => state.innerWidth)
Overall I like the separation between useDividerData
, getThoughtWidths
, and updateDividerWidth
. useDividerData
selects from the Redux state, getThoughtWidths
interacts with the DOM, and updateDividerWidth
calculates the divider width.
However, notice that updateDividerWidth
is really just an extension of useDividerData
, determining which thoughts' widths are relevant based on the Redux state. By splitting up useDividerData
and updateDividerWidth
into separate hooks, it exposes more variables than necessary to the component.
Combining useDividerData
and updateDividerWidth
into a single custom hook that returns ThoughtId[]
would
- reduce the
{ isOnlyChild, isTableView, children, thoughtsAtSameDepth }
return object down to a singleThoughtId[]
- eliminate the custom equality function
- reduce the number of re-renders
Let me know if you see any issues with this suggested change!
*/ | ||
const editingValueUntrimmedStore = reactMinistore<string | null>(null) | ||
|
||
export default editingValueUntrimmedStore |
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.
I agree that they can be combined, since they have the same update rates. That's fine to refactor in a separate PR. Thanks!
src/components/Divider.tsx
Outdated
} | ||
useEffect(() => { | ||
/** Calculates and updates the Divider's width based on sibling thought widths. */ | ||
const updateDividerWidth = () => { |
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.
The multiple return statements and calls to setDivideWidth
make this function look more complicated than it actually is. Try refactoring it so that there is a single setDividerWidth
at the end and no early returns. Math.max(...widths, DIVIDER_MIN_WIDTH))
naturally returns DIVIDER_MIN_WIDTH
when widths = []
.
src/components/Divider.tsx
Outdated
useEffect(() => { | ||
/** Calculates and updates the Divider's width based on sibling thought widths. */ | ||
const updateDividerWidth = () => { | ||
if (!dividerRef.current) return |
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.
This is an exception to my above comment. The initial short circuit to make sure the component is still mounted is fine.
src/components/Divider.tsx
Outdated
/** | ||
* Calculates the width of multiple thoughts by measuring their rendered widths in the DOM. | ||
*/ | ||
const getThoughtWidths = (thoughts: { id: ThoughtId }[]): number[] => { |
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.
Since this function only needs ids, it would be better to reduce the surface area of the parameters and just take ids: ThoughtId[]
.
src/components/Divider.tsx
Outdated
const [dividerWidth, setDividerWidth] = useState<number>(DIVIDER_MIN_WIDTH) | ||
const { isOnlyChild, isTableView, children, thoughtsAtSameDepth } = useDividerData(path) | ||
const editingThoughtId = useSelector((state: State) => state.cursor && head(state.cursor)) | ||
const editingValueUntrimmed = editingValueUntrimmedStore.useSelector(editingValue => editingValue) |
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.
Shorthand for selecting the whole state: editingValueUntrimmedStore.useState()
src/components/Divider.tsx
Outdated
const getThoughtWidths = (thoughts: { id: ThoughtId }[]): number[] => { | ||
return thoughts.map(thought => { | ||
const innerThoughtElement = document.querySelector( | ||
`[aria-label="tree-node"][data-id="${thought.id}"] [aria-label="thought"]`, |
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.
Would it work to use the existing editable element for measurement?
document.querySelector(`[aria-label="editable-${id}"]`)
src/components/LayoutTree.tsx
Outdated
data-id={thoughtId} | ||
data-parent-id={parentId} | ||
data-depth={depth} |
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.
It looks like data-depth
and data-parent-id
are not used.
data-id
may also be able to be removed if the sibling elements are selected by editable aria-label
as mentioned above.
Thank you for your feedback, @raineorshine! I’ve carefully reviewed your suggestions and have made all the necessary changes accordingly. The PR has been updated with the requested modifications, and it is now ready for your next review. Please let me know if there are any further adjustments needed. |
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.
Looks great! Just a few touchups, then we should be good to merge.
src/components/Divider.tsx
Outdated
/** Custom hook to fetch relevant thought IDs for calculating divider width. */ | ||
const useRelevantThoughtIds = (path: Path): ThoughtId[] => { |
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.
Let's call this useWidthDependentThoughtIds
. I try to avoid words like "relevant" or "data" in variable names since they don't explain anything.
src/components/Divider.tsx
Outdated
|
||
return relevantThoughtIds | ||
}, | ||
(prev, next) => prev.length === next.length && prev.every((id, index) => id === next[index]), |
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.
You can use drop in shallowEqual
from react-redux or isEqual
from lodash rather than providing a custom equality function.
src/components/Divider.tsx
Outdated
const isTableView = | ||
(parentId && attributeEquals(state, parentId, '=view', 'Table')) || | ||
(grandParentId && attributeEquals(state, grandParentId, '=view', 'Table')) |
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.
You can make this a little more terse by omitting parentId &&
since attributeEquals
handles null.
src/components/Divider.tsx
Outdated
(state: State) => { | ||
const parentPath = rootedParentOf(state, path) | ||
const parentId = head(parentPath) | ||
const grandParentPath = parentId ? rootedParentOf(state, parentPath) : [] |
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.
It won't affect the behavior here, but []
is not a valid Path
. It should probably be set to null
if there is no parentId
.
src/components/Divider.tsx
Outdated
(parentId && attributeEquals(state, parentId, '=view', 'Table')) || | ||
(grandParentId && attributeEquals(state, grandParentId, '=view', 'Table')) | ||
|
||
const relevantThoughtIds = isOnlyChild |
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.
Thanks, this is much better.
Let's call this dependentThoughtIds
.
Could you also add a couple inline comments that explain the two branches?
This appears to be ready for the next review—awaiting your feedback, @raineorshine. |
…ve code readability.
…n useWidthDependentThoughtIds.
#543
Built on #2763. Ignore commit 99c19d9 to review current changes only.