feat(filters): make the applied-filter panel expandable
A filter clause is shown in a panel above the grid. The panel was clamped to a single line - in the editor with an ellipsis, and in the viewer by letting the clause run the full width of the table - so a long or complex filter could not be read on screen. The panel is now collapsed to one line and carries a chevron that expands it to show the whole clause, wrapped, and collapses it again. The chevron is rendered only when the clause does not fit the collapsed line, so a short filter looks exactly as it did before. Whether it fits is measured from the DOM rather than guessed from the length of the text, since it depends on the rendered width; the measurement is deferred out of the change-detection cycle, and re-run when the clause, the available width, or the state changes. This replaces the editor's hover-only reveal with a real control, which is a button carrying aria-expanded and an accessible label, and gives the viewer the same affordance. Both panels share the styling, which was previously duplicated between the two component blocks. Tested with `client/cypress/e2e/filter-panel.cy.ts`, at a laptop-sized viewport, covering the three states: 1. no filter - the panel is not rendered 2. a short filter - shown in full, with no chevron 3. a long filter (an IN over every value of a free-text column, 646 characters) - collapsed with a chevron, expands to the whole clause over several lines, and collapses again 4. the editor panel behaves the same way for the same clause
This commit is contained in:
@@ -0,0 +1,249 @@
|
|||||||
|
export {}
|
||||||
|
//
|
||||||
|
// The applied-filter panel shows the clause that is in force. A long clause
|
||||||
|
// does not fit the collapsed single line, so the panel carries a chevron that
|
||||||
|
// expands it to show the clause in full - in the viewer and in the editor.
|
||||||
|
//
|
||||||
|
// The chevron is only rendered when the clause actually overflows, so a short
|
||||||
|
// filter looks exactly as it did before.
|
||||||
|
|
||||||
|
const hostUrl = Cypress.env('hosturl')
|
||||||
|
const appLocation = Cypress.env('appLocation')
|
||||||
|
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||||
|
|
||||||
|
context('applied filter panel: ', function () {
|
||||||
|
this.beforeAll(() => {
|
||||||
|
cy.visit(`${hostUrl}/SASLogon/logout`, { timeout: longerCommandTimeout })
|
||||||
|
cy.loginAndUpdateValidKey(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// the panel behaviour depends on how much room the clause has, so pin a
|
||||||
|
// laptop-sized window rather than the wider CI viewport
|
||||||
|
this.beforeEach(() => {
|
||||||
|
cy.viewport(1280, 800)
|
||||||
|
bootApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1 | without a filter the panel is not rendered', () => {
|
||||||
|
openTable('DC_JSLIB.MPE_X_TEST')
|
||||||
|
|
||||||
|
cy.get('.infoBar').should('not.exist')
|
||||||
|
|
||||||
|
cy.screenshot('filter-panel-1-no-filter', { capture: 'viewport' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('2 | a short filter is shown in full, with no chevron', () => {
|
||||||
|
openTable('DC_JSLIB.MPE_X_TEST')
|
||||||
|
|
||||||
|
openFilterPopup()
|
||||||
|
setFilterVariable('SOME_DROPDOWN')
|
||||||
|
setFilterValue('Option 1')
|
||||||
|
submitFilter()
|
||||||
|
|
||||||
|
cy.get('.infoBar-text').should('contain.text', "SOME_DROPDOWN = 'Option 1'")
|
||||||
|
// the clause fits the collapsed line, so there is nothing to expand
|
||||||
|
assertClauseFitsCollapsedLine()
|
||||||
|
cy.get('.infoBar-toggle').should('not.exist')
|
||||||
|
|
||||||
|
cy.screenshot('filter-panel-2-filter', { capture: 'viewport' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('3 | a long filter is collapsed to a chevron and expands to the full clause', () => {
|
||||||
|
openTable('DC_JSLIB.MPE_X_TEST')
|
||||||
|
|
||||||
|
// SOME_CHAR holds a very long free-text value, so an IN over every value
|
||||||
|
// produces a clause of several hundred characters
|
||||||
|
openFilterPopup()
|
||||||
|
setFilterVariable('SOME_CHAR')
|
||||||
|
setFilterOperator('IN')
|
||||||
|
chooseAllFilterValues()
|
||||||
|
submitFilter()
|
||||||
|
|
||||||
|
cy.get('.infoBar-text').should('contain.text', 'SOME_CHAR IN')
|
||||||
|
assertClauseOverflowsCollapsedLine()
|
||||||
|
cy.get('.infoBar-toggle')
|
||||||
|
.should('exist')
|
||||||
|
.and('have.attr', 'aria-expanded', 'false')
|
||||||
|
|
||||||
|
cy.screenshot('filter-panel-3-big-filter-collapsed', {
|
||||||
|
capture: 'viewport'
|
||||||
|
})
|
||||||
|
|
||||||
|
// expand: the whole clause is rendered, on several lines, unclipped
|
||||||
|
cy.get('.infoBar-toggle').click()
|
||||||
|
cy.get('.infoBar').should('have.class', 'expanded')
|
||||||
|
cy.get('.infoBar-toggle').should('have.attr', 'aria-expanded', 'true')
|
||||||
|
assertClauseFullyVisible()
|
||||||
|
assertClauseSpansSeveralLines()
|
||||||
|
|
||||||
|
cy.screenshot('filter-panel-3-big-filter-expanded', { capture: 'viewport' })
|
||||||
|
|
||||||
|
// collapse again
|
||||||
|
cy.get('.infoBar-toggle').click()
|
||||||
|
cy.get('.infoBar').should('not.have.class', 'expanded')
|
||||||
|
assertClauseOverflowsCollapsedLine()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('4 | the editor panel collapses and expands in the same way', () => {
|
||||||
|
openTable('DC_JSLIB.MPE_X_TEST')
|
||||||
|
|
||||||
|
openFilterPopup()
|
||||||
|
setFilterVariable('SOME_CHAR')
|
||||||
|
setFilterOperator('IN')
|
||||||
|
chooseAllFilterValues()
|
||||||
|
submitFilter()
|
||||||
|
|
||||||
|
// the editor shows the filter the viewer just stored
|
||||||
|
cy.url().then((url) => {
|
||||||
|
const filterId = url.split('/').pop()
|
||||||
|
cy.visit(
|
||||||
|
`${hostUrl}${appLocation}/#/editor/DC_JSLIB.MPE_X_TEST/${filterId}`,
|
||||||
|
{ timeout: longerCommandTimeout }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('.infoBar-text', { timeout: longerCommandTimeout }).should(
|
||||||
|
'contain.text',
|
||||||
|
'SOME_CHAR IN'
|
||||||
|
)
|
||||||
|
assertClauseOverflowsCollapsedLine()
|
||||||
|
|
||||||
|
cy.get('.infoBar-toggle').click()
|
||||||
|
cy.get('.infoBar').should('have.class', 'expanded')
|
||||||
|
assertClauseFullyVisible()
|
||||||
|
assertClauseSpansSeveralLines()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cypress clears cookies between tests, which drops the SASjs Server session
|
||||||
|
* and lands the app back on the evaluation agreement card. Accept it when it is
|
||||||
|
* shown - a no-op when the session survived.
|
||||||
|
*/
|
||||||
|
const bootApp = () => {
|
||||||
|
cy.visit(hostUrl, { timeout: longerCommandTimeout })
|
||||||
|
cy.get('body').then(($body: any) => {
|
||||||
|
if ($body.find('#TCS input[type="checkbox"]').length) {
|
||||||
|
cy.get('#TCS input[type="checkbox"]').check({ force: true })
|
||||||
|
cy.wait(4000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens a table in the viewer and waits for its grid to render. */
|
||||||
|
const openTable = (libMem: string) => {
|
||||||
|
cy.visit(`${hostUrl}${appLocation}/#/view/data/${libMem}`, {
|
||||||
|
timeout: longerCommandTimeout
|
||||||
|
})
|
||||||
|
cy.get('.filterSide', { timeout: longerCommandTimeout }).should('exist')
|
||||||
|
cy.wait(2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openFilterPopup = () => {
|
||||||
|
cy.get('.filterSide', { timeout: longerCommandTimeout }).click()
|
||||||
|
cy.get('clr-dropdown-menu', { timeout: longerCommandTimeout })
|
||||||
|
.contains('Filter')
|
||||||
|
.click()
|
||||||
|
cy.get('.filter-modal', { timeout: longerCommandTimeout }).should(
|
||||||
|
'be.visible'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The soft-select inputs drop a transparent .overlay click-catcher over the
|
||||||
|
* modal while their suggestion list is open, so they need force.
|
||||||
|
*/
|
||||||
|
const setFilterVariable = (column: string) => {
|
||||||
|
cy.get('#vals_var_id0_0').clear({ force: true }).type(column, { force: true })
|
||||||
|
cy.get('#datalist_vals_var_id0_0 option').contains(column).click({
|
||||||
|
force: true
|
||||||
|
})
|
||||||
|
cy.get('#vals_var_id0_0').trigger('keyup', { key: 'Escape', force: true })
|
||||||
|
cy.wait(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFilterValue = (value: string) => {
|
||||||
|
cy.get('#vals_0_0').clear({ force: true }).type(value, { force: true })
|
||||||
|
cy.get('#vals_0_0').trigger('keyup', { key: 'Escape', force: true })
|
||||||
|
cy.wait(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFilterOperator = (operator: string) => {
|
||||||
|
cy.get('.filter-modal .operator-col select').first().select(operator)
|
||||||
|
cy.wait(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IN/NOT IN take their values from a modal of checkboxes - take them all. */
|
||||||
|
const chooseAllFilterValues = () => {
|
||||||
|
cy.contains('.filter-modal button', 'Choose values').click()
|
||||||
|
cy.get('.in-values-modal', { timeout: longerCommandTimeout }).should(
|
||||||
|
'be.visible'
|
||||||
|
)
|
||||||
|
cy.get('.in-values-modal input[type=checkbox]').then(($checkboxes: any) => {
|
||||||
|
for (let i = 0; i < $checkboxes.length; i++) {
|
||||||
|
cy.get('.in-values-modal input[type=checkbox]')
|
||||||
|
.eq(i)
|
||||||
|
.click({ force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.contains('.in-values-modal button', 'Apply').click()
|
||||||
|
cy.wait(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitFilter = () => {
|
||||||
|
cy.contains('.filter-modal button', 'Ok').click()
|
||||||
|
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
||||||
|
cy.wait(2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The clause is wider than the collapsed line, so it is clipped. */
|
||||||
|
const assertClauseOverflowsCollapsedLine = () => {
|
||||||
|
cy.get('.infoBar-text').then(($text: any) => {
|
||||||
|
const el = $text[0] as HTMLElement
|
||||||
|
expect(
|
||||||
|
el.scrollWidth,
|
||||||
|
'the clause is wider than the collapsed line'
|
||||||
|
).to.be.greaterThan(el.clientWidth)
|
||||||
|
expect(
|
||||||
|
el.ownerDocument.defaultView!.getComputedStyle(el).whiteSpace,
|
||||||
|
'collapsed panel does not wrap'
|
||||||
|
).to.equal('nowrap')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The clause fits the collapsed line, so nothing is hidden. */
|
||||||
|
const assertClauseFitsCollapsedLine = () => {
|
||||||
|
cy.get('.infoBar-text').then(($text: any) => {
|
||||||
|
const el = $text[0] as HTMLElement
|
||||||
|
expect(el.scrollWidth, 'the clause fits the collapsed line').to.be.at.most(
|
||||||
|
el.clientWidth
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expanded, the whole clause is rendered and nothing is clipped. */
|
||||||
|
const assertClauseFullyVisible = () => {
|
||||||
|
cy.get('.infoBar-text').then(($text: any) => {
|
||||||
|
const el = $text[0] as HTMLElement
|
||||||
|
const style = el.ownerDocument.defaultView!.getComputedStyle(el)
|
||||||
|
expect(style.whiteSpace, 'expanded panel wraps').to.equal('normal')
|
||||||
|
expect(el.scrollWidth, 'expanded panel clips nothing').to.be.at.most(
|
||||||
|
el.clientWidth
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expanded, the clause occupies several lines rather than one. */
|
||||||
|
const assertClauseSpansSeveralLines = () => {
|
||||||
|
cy.get('.infoBar-text').then(($text: any) => {
|
||||||
|
const el = $text[0] as HTMLElement
|
||||||
|
const lineHeight =
|
||||||
|
parseFloat(
|
||||||
|
el.ownerDocument.defaultView!.getComputedStyle(el).lineHeight
|
||||||
|
) || 20
|
||||||
|
expect(
|
||||||
|
el.clientHeight / lineHeight,
|
||||||
|
'the clause spans several lines'
|
||||||
|
).to.be.greaterThan(3)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -369,11 +369,30 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!['', ' '].includes(queryText)) {
|
@if (!['', ' '].includes(queryText)) {
|
||||||
<div class="clr-col-md-12 infoBar">
|
<div
|
||||||
<span
|
#infoBar
|
||||||
|
class="clr-col-md-12 infoBar"
|
||||||
|
[class.expanded]="filterExpanded"
|
||||||
|
>
|
||||||
|
<span class="infoBar-text"
|
||||||
>FILTER :
|
>FILTER :
|
||||||
<b>{{ queryText }}</b>
|
<b>{{ queryText }}</b>
|
||||||
</span>
|
</span>
|
||||||
|
@if (filterOverflows) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="infoBar-toggle"
|
||||||
|
[attr.aria-expanded]="filterExpanded"
|
||||||
|
[attr.aria-label]="
|
||||||
|
filterExpanded
|
||||||
|
? 'Collapse the filter clause'
|
||||||
|
: 'Expand the filter clause'
|
||||||
|
"
|
||||||
|
(click)="toggleFilterPanel()"
|
||||||
|
>
|
||||||
|
<clr-icon aria-hidden="true" shape="caret down"></clr-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AfterViewChecked,
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
@@ -112,7 +113,9 @@ import { ParseResult } from '../models/ParseResult.interface'
|
|||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class EditorComponent
|
||||||
|
implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy
|
||||||
|
{
|
||||||
@ViewChildren('uploadStater')
|
@ViewChildren('uploadStater')
|
||||||
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
|
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
|
||||||
@ViewChildren('queryFilter')
|
@ViewChildren('queryFilter')
|
||||||
@@ -525,6 +528,19 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
public tableData: Array<any> = []
|
public tableData: Array<any> = []
|
||||||
public queryText = ''
|
public queryText = ''
|
||||||
public queryTextSaved = ''
|
public queryTextSaved = ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The applied-filter panel is collapsed to a single line by default. The
|
||||||
|
* chevron that expands it is only useful when the clause does not fit that
|
||||||
|
* line, which depends on the rendered width - so it is measured from the DOM
|
||||||
|
* rather than guessed from the length of the text.
|
||||||
|
*/
|
||||||
|
public filterExpanded = false
|
||||||
|
public filterOverflows = false
|
||||||
|
|
||||||
|
@ViewChild('infoBar') infoBar?: ElementRef<HTMLElement>
|
||||||
|
|
||||||
|
private filterMeasuredKey = ''
|
||||||
public showApprovers = false
|
public showApprovers = false
|
||||||
public pkDups = false
|
public pkDups = false
|
||||||
public validationDone = 0
|
public validationDone = 0
|
||||||
@@ -3547,6 +3563,48 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked() {
|
||||||
|
this.scheduleFilterOverflowCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures whether the applied-filter clause fits the collapsed single line,
|
||||||
|
* and shows or hides the chevron accordingly. The measurement is deferred
|
||||||
|
* because `filterOverflows` and `filterExpanded` are bound in this view -
|
||||||
|
* setting them inside the change-detection cycle would raise
|
||||||
|
* ExpressionChangedAfterItHasBeenChecked.
|
||||||
|
*/
|
||||||
|
private scheduleFilterOverflowCheck() {
|
||||||
|
const el = this.infoBar?.nativeElement
|
||||||
|
if (!el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = el.querySelector('.infoBar-text') as HTMLElement | null
|
||||||
|
if (!text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-measure only when the clause, the available width, or the state changes
|
||||||
|
const key = `${this.queryText}|${text.clientWidth}|${this.filterExpanded}`
|
||||||
|
if (key === this.filterMeasuredKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.filterMeasuredKey = key
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// while expanded the clause wraps, so there is nothing to measure
|
||||||
|
if (this.filterExpanded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.filterOverflows = text.scrollWidth > text.clientWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleFilterPanel() {
|
||||||
|
this.filterExpanded = !this.filterExpanded
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// Fix ARIA accessibility issues after table initialization
|
// Fix ARIA accessibility issues after table initialization
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -499,10 +499,29 @@
|
|||||||
!['', ' '].includes(queryText) &&
|
!['', ' '].includes(queryText) &&
|
||||||
!abortActive
|
!abortActive
|
||||||
) {
|
) {
|
||||||
<div class="clr-col-md-12 infoBar">
|
<div
|
||||||
<span
|
#infoBar
|
||||||
|
class="clr-col-md-12 infoBar"
|
||||||
|
[class.expanded]="filterExpanded"
|
||||||
|
>
|
||||||
|
<span class="infoBar-text"
|
||||||
>FILTER : <b>{{ queryText }}</b></span
|
>FILTER : <b>{{ queryText }}</b></span
|
||||||
>
|
>
|
||||||
|
@if (filterOverflows) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="infoBar-toggle"
|
||||||
|
[attr.aria-expanded]="filterExpanded"
|
||||||
|
[attr.aria-label]="
|
||||||
|
filterExpanded
|
||||||
|
? 'Collapse the filter clause'
|
||||||
|
: 'Expand the filter clause'
|
||||||
|
"
|
||||||
|
(click)="toggleFilterPanel()"
|
||||||
|
>
|
||||||
|
<clr-icon aria-hidden="true" shape="caret down"></clr-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
AfterContentInit,
|
AfterContentInit,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
|
AfterViewChecked,
|
||||||
|
ElementRef,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
QueryList,
|
QueryList,
|
||||||
@@ -58,7 +60,7 @@ import { buildColInfoHtml } from '../shared/utils/col-info-html'
|
|||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViewerComponent
|
export class ViewerComponent
|
||||||
implements AfterContentInit, AfterViewInit, OnDestroy
|
implements AfterContentInit, AfterViewInit, AfterViewChecked, OnDestroy
|
||||||
{
|
{
|
||||||
@ViewChildren('queryFilter')
|
@ViewChildren('queryFilter')
|
||||||
queryFilterCompList: QueryList<QueryComponent> = new QueryList()
|
queryFilterCompList: QueryList<QueryComponent> = new QueryList()
|
||||||
@@ -96,6 +98,19 @@ export class ViewerComponent
|
|||||||
public libTab!: string
|
public libTab!: string
|
||||||
public queryText: string = ''
|
public queryText: string = ''
|
||||||
public webQueryText: string = ''
|
public webQueryText: string = ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The applied-filter panel is collapsed to a single line by default. The
|
||||||
|
* chevron that expands it is only useful when the clause does not fit that
|
||||||
|
* line, which depends on the rendered width - so it is measured from the DOM
|
||||||
|
* rather than guessed from the length of the text.
|
||||||
|
*/
|
||||||
|
public filterExpanded = false
|
||||||
|
public filterOverflows = false
|
||||||
|
|
||||||
|
@ViewChild('infoBar') infoBar?: ElementRef<HTMLElement>
|
||||||
|
|
||||||
|
private filterMeasuredKey = ''
|
||||||
public submitLoading!: boolean
|
public submitLoading!: boolean
|
||||||
public queryErr: boolean = false
|
public queryErr: boolean = false
|
||||||
public queryErrMessage!: string
|
public queryErrMessage!: string
|
||||||
@@ -1445,6 +1460,48 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked() {
|
||||||
|
this.scheduleFilterOverflowCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures whether the applied-filter clause fits the collapsed single line,
|
||||||
|
* and shows or hides the chevron accordingly. The measurement is deferred
|
||||||
|
* because `filterOverflows` and `filterExpanded` are bound in this view -
|
||||||
|
* setting them inside the change-detection cycle would raise
|
||||||
|
* ExpressionChangedAfterItHasBeenChecked.
|
||||||
|
*/
|
||||||
|
private scheduleFilterOverflowCheck() {
|
||||||
|
const el = this.infoBar?.nativeElement
|
||||||
|
if (!el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = el.querySelector('.infoBar-text') as HTMLElement | null
|
||||||
|
if (!text) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-measure only when the clause, the available width, or the state changes
|
||||||
|
const key = `${this.queryText}|${text.clientWidth}|${this.filterExpanded}`
|
||||||
|
if (key === this.filterMeasuredKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.filterMeasuredKey = key
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// while expanded the clause wraps, so there is nothing to measure
|
||||||
|
if (this.filterExpanded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.filterOverflows = text.scrollWidth > text.clientWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleFilterPanel() {
|
||||||
|
this.filterExpanded = !this.filterExpanded
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// Fix ARIA accessibility issues after table initialization
|
// Fix ARIA accessibility issues after table initialization
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
+52
-29
@@ -120,30 +120,6 @@ app-editor {
|
|||||||
|
|
||||||
.infoBar {
|
.infoBar {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
background: #495967;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 3px;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
height: 30px;
|
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
span {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
height: unset;
|
|
||||||
white-space: normal;
|
|
||||||
|
|
||||||
span {
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pkHeader {
|
.pkHeader {
|
||||||
@@ -1228,11 +1204,6 @@ app-viewer {
|
|||||||
|
|
||||||
.infoBar {
|
.infoBar {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
background: #495967;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 3px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterSide {
|
.filterSide {
|
||||||
@@ -5448,3 +5419,55 @@ body[cds-theme='dark'] {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The applied-filter panel. Collapsed to a single line by default so that a
|
||||||
|
long filter does not take over the header; the chevron - rendered only when
|
||||||
|
the clause does not fit that line - expands the panel to show it in full. */
|
||||||
|
.infoBar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: #495967;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.infoBar-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.infoBar-text {
|
||||||
|
overflow: visible;
|
||||||
|
white-space: normal;
|
||||||
|
text-overflow: clip;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoBar-toggle {
|
||||||
|
flex: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 2px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
clr-icon {
|
||||||
|
transition: transform 0.15s ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded .infoBar-toggle clr-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user