Skip to content

Commit

Permalink
feat: Parent Visibility Toggles should scroll nicely (PT-187578056) (#…
Browse files Browse the repository at this point in the history
…1275)

[#187578056](https://www.pivotaltracker.com/story/show/187578056)

When the list of toggle buttons is wider than the available space, these changes ensure that:

- The last button in the list is visible when the Parent Visibility Toggles option is activated
- The left and right arrows appear only when the list is scrollable in their associated direction
- All currently visible buttons are fully visible with no cropping by the boundaries of the button list container
  • Loading branch information
emcelroy authored May 22, 2024
1 parent 49dc912 commit 715a165
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 48 deletions.
30 changes: 15 additions & 15 deletions v3/cypress/e2e/graph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,31 +208,32 @@ context("Graph UI", () => {
cy.get("[data-testid=parent-toggles-last]").should("exist").and("have.text", "☐ Last")
cy.get("[data-testid=graph]").click()
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Asian Elephant").should("exist").and("not.have.class", "case-hidden")
.find("button").contains("Spotted Hyena").should("exist").and("not.have.class", "case-hidden")
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Asian Elephant").click()
.find("button").contains("Spotted Hyena").click()
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Asian Elephant").should("have.class", "case-hidden")
.find("button").contains("Spotted Hyena").should("have.class", "case-hidden")
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Asian Elephant").click()
.find("button").contains("Spotted Hyena").click()
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Asian Elephant").should("not.have.class", "case-hidden")
.find("button").contains("Spotted Hyena").should("not.have.class", "case-hidden")
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("African Elephant").should("exist").and("be.visible")
.find("button").contains("Red Fox").should("exist").and("be.visible")
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Chimpanzee").should("exist").and("not.be.visible")
.find("button").contains("Owl Monkey").should("exist").and("not.be.visible")
cy.get("[data-testid=parent-toggles-case-buttons-left]").should("exist")
cy.get("[data-testid=parent-toggles-case-buttons-right]").should("exist")
cy.get("[data-testid=parent-toggles-case-buttons-right]").click().click()
cy.get("[data-testid=parent-toggles-case-buttons-right]").should("not.exist")
cy.get("[data-testid=parent-toggles-case-buttons-left]").click()
cy.wait(250)
cy.get("[data-testid=parent-toggles-case-buttons-right]").should("exist")
cy.get("[data-testid=parent-toggles-case-buttons-list]").find("button")
.contains("African Elephant").should("exist").and("not.be.visible")
.contains("Red Fox").should("exist").and("not.be.visible")
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Chimpanzee").should("exist").and("be.visible")
cy.get("[data-testid=parent-toggles-case-buttons-left]").click().click()
.find("button").contains("Owl Monkey").should("exist").and("be.visible")
cy.get("[data-testid=parent-toggles-case-buttons-right]").click()
cy.wait(250)
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("African Elephant").should("exist").and("be.visible")
.find("button").contains("Red Fox").should("exist").and("be.visible")
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("Gray Wolf").should("exist").and("not.be.visible")
cy.get("[data-testid=parent-toggles-last]").click()
Expand All @@ -259,8 +260,7 @@ context("Graph UI", () => {
cy.get("[data-testid=parent-toggles-last]").click()
cy.wait(250)
cy.get("[data-testid=parent-toggles-last]").should("have.text", "☒ Last")
cy.get("[data-testid=parent-toggles-case-buttons-list]")
.find("button").contains("African Elephant").click()
cy.get("[data-testid=parent-toggles-case-buttons-list]").find("button").contains("Red Fox").click()
cy.get("[data-testid=parent-toggles-last]").should("have.text", "☐ Last")
// TODO: Figure out why the below doesn't work in Cypress -- some buttons aren't being set to 'case-hidden' when
// Hide All is clicked. It seems to work fine in a web browser, though.
Expand Down
95 changes: 62 additions & 33 deletions v3/src/components/graph/components/parent-toggles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface ICreateCaseButtons {

const SCROLL_BUTTON_WIDTH = 32
const TEXT_OFFSET = 5
const BUTTON_FONT = `11px Montserrat, sans-serif`
const BUTTON_FONT = "11px Montserrat, sans-serif"

const consolidateCaseButtonsByAttrValue = (caseButtons: ICaseButton[], hiddenCases: string[]): ICaseButton[] => {
// Use caseButtons to create a map of attribute values to matching case IDs
Expand Down Expand Up @@ -94,38 +94,53 @@ export const ParentToggles = observer(function ParentToggles() {
const [buttonContainerWidth, setButtonContainerWidth] = useState(0)

// Returns the currently available width for the button list, the total width of the buttons that can fit within
// that available width from the given `firstIndex`, and the index of the last visible button.
const buttonContainerDetails = useCallback((firstIndex: number) => {
// that available width, and the index of the first visible button.
const buttonContainerDetails = useCallback(() => {
let buttonsVisibleWidth = 0
let lastIndex = 0
const usedWidth = toggleTextWidth + TEXT_OFFSET + lastButtonWidth + TEXT_OFFSET + SCROLL_BUTTON_WIDTH * 2
const availableWidth = tileWidth - usedWidth
let firstIndex = 0
const usedWidth = toggleTextWidth + TEXT_OFFSET + lastButtonWidth + TEXT_OFFSET
let availableWidth = tileWidth - usedWidth
const needScrollButtons = caseButtonsListWidth > availableWidth

for (let i = firstIndex; i < caseButtons.length; i++) {
if (needScrollButtons) {
const rightScrollButtonWidth = lastVisibleIndex.current < caseButtons.length - 1 ? SCROLL_BUTTON_WIDTH : 0
const leftScrollButtonWidth = firstVisibleIndex.current !== 0 ? SCROLL_BUTTON_WIDTH : 0
availableWidth = availableWidth - rightScrollButtonWidth - leftScrollButtonWidth
}

// Determine how many buttons can fit within the available width starting from the last visible button
// and working backwards.
for (let i = lastVisibleIndex.current; i >= 0; i--) {
buttonsVisibleWidth += caseButtons[i].width + TEXT_OFFSET
if (buttonsVisibleWidth > availableWidth) {
lastIndex = i - 1
firstIndex = i + 1
buttonsVisibleWidth -= caseButtons[i].width + TEXT_OFFSET
break
}
lastIndex = i
firstIndex = i
}

return { availableWidth, buttonsVisibleWidth, lastIndex }
}, [caseButtons, lastButtonWidth, tileWidth, toggleTextWidth])
return { availableWidth, buttonsVisibleWidth, firstIndex }
}, [caseButtons, caseButtonsListWidth, lastButtonWidth, tileWidth, toggleTextWidth])

useEffect(function updateButtonContainerWidth() {
const { availableWidth, buttonsVisibleWidth, lastIndex } = buttonContainerDetails(firstVisibleIndex.current)
const { availableWidth, buttonsVisibleWidth, firstIndex } = buttonContainerDetails()
// Find the offset by summing the widths of all buttons before the first visible button
let offset = 0
for (let i = 0; i < firstIndex; i++) {
offset += caseButtons[i].width + TEXT_OFFSET
}
setButtonContainerWidth(buttonsVisibleWidth)
setButtonsListOffset(-offset)
firstVisibleIndex.current = firstIndex
if (caseButtonsListWidth > availableWidth) {
lastVisibleIndex.current = lastIndex
setShowRightButton(true)
setShowLeftButton(true)
setShowRightButton(lastVisibleIndex.current !== caseButtons.length - 1)
setShowLeftButton(firstVisibleIndex.current !== 0)
} else {
setShowRightButton(false)
setShowLeftButton(false)
}
}, [buttonContainerDetails, caseButtonsListWidth, tileWidth])
}, [buttonContainerDetails, caseButtons, caseButtonsListWidth, tileWidth])

const handleToggleAll = () => {
if (hiddenCases.length > 0) {
Expand Down Expand Up @@ -190,27 +205,41 @@ export const ParentToggles = observer(function ParentToggles() {
}

const handleScroll = (direction: "left" | "right") => {
const isScrollingRight = direction === "right"
const boundaryIndex = isScrollingRight ? lastVisibleIndex.current : firstVisibleIndex.current
const limitIndex = isScrollingRight ? caseButtons.length - 1 : 0
if (boundaryIndex === limitIndex) return
const { availableWidth } = buttonContainerDetails()
let newOffset = 0
let buttonsVisibleWidth = 0
const increment = direction === "right" ? 1 : -1
const startIndex = direction === "right" ? lastVisibleIndex.current + 1 : firstVisibleIndex.current - 1
const endIndex = direction === "right" ? caseButtons.length : -1

const nextButtonIndex = isScrollingRight ? boundaryIndex + 1 : boundaryIndex - 1
const nextButtonOffset = caseButtons[nextButtonIndex].width + TEXT_OFFSET
const offset = isScrollingRight ? -nextButtonOffset : nextButtonOffset
const additionalOffset = !isScrollingRight && lastVisibleIndex.current === caseButtons.length - 1
? lastButtonWidth + TEXT_OFFSET + SCROLL_BUTTON_WIDTH * 2
: 0
const fullOffset = buttonsListOffset + offset - additionalOffset
setButtonsListOffset(fullOffset)
for (let i = startIndex; i !== endIndex; i += increment) {
const buttonWidth = caseButtons[i].width + TEXT_OFFSET
buttonsVisibleWidth += buttonWidth

if (buttonsVisibleWidth > availableWidth) {
buttonsVisibleWidth -= buttonWidth
break
}

newOffset += buttonWidth

if (isScrollingRight) {
lastVisibleIndex.current++
firstVisibleIndex.current++
if (direction === "right") {
firstVisibleIndex.current++
lastVisibleIndex.current++
} else {
firstVisibleIndex.current--
lastVisibleIndex.current--
}
}
if (direction === "right") {
setButtonsListOffset(prevOffset => prevOffset - newOffset)
} else {
firstVisibleIndex.current--
lastVisibleIndex.current--
setButtonsListOffset(prevOffset => prevOffset + newOffset)
}

setButtonContainerWidth(buttonsVisibleWidth)
setShowLeftButton(firstVisibleIndex.current > 0)
setShowRightButton(lastVisibleIndex.current < caseButtons.length - 1)
}

const renderCaseButtons = () => {
Expand Down

0 comments on commit 715a165

Please sign in to comment.