UNPKG

@ux-aspects/ux-aspects

Version:

Open source user interface framework for building modern, responsive, mobile big data applications

685 lines 116 kB
/* eslint-disable @typescript-eslint/no-empty-object-type */ import { LiveAnnouncer } from '@angular/cdk/a11y'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, inject, Input, NgZone, Output, } from '@angular/core'; import { hierarchy, partition, scaleLinear, select, transition, } from 'd3'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ContrastService, FocusIndicatorOriginService } from '../../directives/accessibility/index'; import { ResizeService } from '../../directives/resize/index'; import { ColorService, ThemeColor } from '../../services/color/index'; import * as i0 from "@angular/core"; import * as i1 from "../../directives/accessibility/focus-indicator/focus-indicator.directive"; import * as i2 from "@angular/common"; export class PartitionMapComponent { constructor() { this._colorService = inject(ColorService); this._elementRef = inject(ElementRef); this._changeDetector = inject(ChangeDetectorRef); this._ngZone = inject(NgZone); this._focusOrigin = inject(FocusIndicatorOriginService); this._contrastRatio = inject(ContrastService); this._liveAnnouncer = inject(LiveAnnouncer); this._resizeService = inject(ResizeService); this._segmentCache = new WeakMap(); /** Determine the pixel height of collapsed segments. */ this.collapsedHeight = 40; /** Define a minimum desired pixel width for a segment. */ this.minSegmentWidth = 5; /** Define the function that will return the aria announcement for a given segment. */ this.segmentAnnouncement = this.defaultSegmentAnnouncement; /** Emits whenever a segment is selected. */ this.selectedChange = new EventEmitter(); /** Store the processed segments */ this._segments = []; /** Store the specified color sequences */ this._colors = [[]]; /** Store the assigned colors for each segment */ this._segmentColors = new Map(); /** Store the visible x scale */ this._x = scaleLinear().range([0, 100]); /** Store the visible y scale */ this._y = scaleLinear().range([0, 100]); /** Store the width of the chart on resize to avoid any reflow */ this._width = this._elementRef.nativeElement.offsetWidth; /** Store the height of the chart on resize to avoid any reflow */ this._height = this._elementRef.nativeElement.offsetHeight; /** Flag to determine when the inputs have all been bound */ this._initialized = false; /** Unsubscribe from any observables on destroy */ this._onDestroy = new Subject(); } /** Define the colors to be used for each row and the order they should appear. */ set colors(colors) { this._colors = colors; // clear the save color mappings this._segmentColors.clear(); } /** Define the dataset to display in the chart. */ set dataset(dataset) { // store the current dataset this._dataset = dataset; // clear any existing color assignments this._segmentColors.clear(); // update the segment layout this.setDataset(dataset); } get dataset() { return this._dataset; } /** Define the currently selected item. */ set selected(selected) { // if this is set before the dataset is process then store it to be selected later if (this._segments.length === 0) { this._awaitingSelection = selected; return; } // perform the selection this.select(this.getHierarchyNodeFromSegment(selected)); } ngOnInit() { this._resizeService .addResizeListener(this._elementRef.nativeElement) .pipe(takeUntil(this._onDestroy)) .subscribe(dimensions => { this._width = dimensions.width; this._height = dimensions.height; this._changeDetector.detectChanges(); // set our new ranges if (this._selected) { this._x.domain([ this.getSegmentX(this._selected), this.getSegmentX(this._selected) + this.getSegmentWidth(this._selected), ]); this._y.domain([this._selected.y0, 1]).range([this.getTotalCollapsedHeight(), 100]); } // render the chart to ensure positions and sizes are correct this.updateSegments(); }); this._initialized = true; // Run again so that the colors get applied this._changeDetector.detectChanges(); } ngOnDestroy() { this._resizeService.removeResizeListener(this._elementRef.nativeElement); this._onDestroy.next(); this._onDestroy.complete(); } /** Handle segment clicks */ _onSegmentSelect(segment) { // if the clicked node is already selected, navigate to the parent node this.select(this._isSelected(segment) && segment.parent ? segment.parent : segment); } /** Get the background color for a given segment */ _getBackgroundColor(segment) { // This can be called before `colors` is initialized, in which case return the default color if (!this._initialized) { return '#fff'; } // each segment has a determinable color key based on the name and depth const key = `${segment.data.name} - ${segment.depth}`; // check if a segment with the same name (and depth) has previously if (this._segmentColors.has(key)) { return this._segmentColors.get(key); } // get the corresponding row of colors const sequence = this.getColorSequence(segment.depth); // if the sequence has not been specified return a default of white if (!sequence || sequence.length === 0) { return '#fff'; } // get siblings const siblings = this.getAllSiblings(segment); // get the previous sibling if there is one const sibling = siblings[siblings.indexOf(segment) - 1]; // if there is a previous sibling then get its color and use the next one in the sequence if (sibling) { const index = sequence.indexOf(this._getBackgroundColor(sibling)); const color = sequence[(index + 1) % sequence.length]; // store the color by key this._segmentColors.set(key, color); return color; } // store the color by key this._segmentColors.set(key, sequence[0]); // if there is no previous sibling then simply return the first color in the sequence return sequence[0]; } /** Get the tab index of a segment */ _getTabIndex(segment) { return segment === this._focusableSegment ? 0 : -1; } /** Shift focus to the parent segment */ _focusParent(segment) { // if there is no parent (ie, we are the root segment) then retain focus if (!segment.parent) { return; } // otherwise focus the parent this.focusSegment(segment.parent); } /** Shift focus to the child segment */ _focusChild(segment) { // if there are no children (ie, we are a leaf segment) then retain focus if (!segment.children) { return; } // find the first visible child const child = segment.children.find(_segment => this.isVisible(_segment)); // otherwise focus the first visible child if (child) { this.focusSegment(child); } } /** Shift focus to the sibling segment */ _focusSibling(segment, delta) { // if we are the root node then do nothing if (!segment.parent) { return; } // get a list of all the siblings (at the same row regardless of the same parent) const siblings = this.getAllSiblings(segment); // get the index of the segment in the list of siblings const index = siblings.indexOf(segment); // get the target sibling const sibling = siblings[index + delta]; // ensure the sibling is visible otherwise we can't select it if (!sibling || !this.isVisible(sibling)) { return; } // otherwise focus the sibling this.focusSegment(sibling); } _focusFirstSibling(segment) { // if we are the root node then do nothing if (!segment.parent) { return; } // get a list of all the siblings (at the same row regardless of the same parent) const siblings = this.getAllSiblings(segment); // find the first visible sibling const sibling = siblings.find(_sibling => this.isVisible(_sibling)); // ensure there is a sibling if (!sibling) { return; } // otherwise focus the sibling this.focusSegment(sibling); } _focusLastSibling(segment) { // if we are the root node then do nothing if (!segment.parent) { return; } // get a list of all the siblings (at the same row regardless of the same parent) const siblings = this.getAllSiblings(segment); // find the last visible sibling const sibling = siblings.reverse().find(_sibling => this.isVisible(_sibling)); // ensure there is a sibling if (!sibling) { return; } // otherwise focus the sibling this.focusSegment(sibling); } /** Determine if a given segment is currently collapsed */ _isCollapsed(segment) { return this._selected && segment.depth < this._selected.depth; } /** Determine if a given segment is currently selected */ _isSelected(segment) { return this._selected === segment; } /** Get the contast color class for the segment */ _getContrastColor(segment) { const backgroundColor = this._getBackgroundColor(segment); const lightColor = ThemeColor.parse('#fff'); const darkColor = ThemeColor.parse('#000'); const color = this._contrastRatio.getContrastColor(ThemeColor.parse(backgroundColor), lightColor, darkColor); return color === lightColor ? 'partition-map-segment-light' : 'partition-map-segment-dark'; } /** Provide an aria announcement when the node is focused */ _onFocus(segment) { // get all ancestors const ancestors = segment.ancestors().map(ancestor => ancestor.data); // get the current node and the parent nodes const [item, ...parents] = ancestors; // get the hierarchy node data from the item const hierarchichalItem = this.getHierarchyNodeFromSegment(item); // get the function that creates the announcement const announcement = this.segmentAnnouncement({ item, parents, value: this._getSegmentValue(segment.data), collapsed: this._isCollapsed(hierarchichalItem), selected: this._isSelected(hierarchichalItem), }); // make aria announcement this._liveAnnouncer.announce(announcement); } /** Determine if the content is smaller than the width of an ellipsis */ _getSegmentContentHidden(segment) { // get the width of the segment as a pixel value const width = (this._width / 100) * this.getNormalizedSegmentWidth(segment); // if the width is less than 50 px hide the content return width < 50; } /** Get the value of a segment based on the accumulation of all child values */ _getSegmentValue(segment) { // it it has a value then return the value // eslint-disable-next-line no-prototype-builtins if (segment.hasOwnProperty('value')) { return segment.value; } return segment.children.reduce((value, child) => value + this._getSegmentValue(child), 0); } _getContext(segment) { const context = { segment: segment.data, value: this._getSegmentValue(segment.data), color: this._getBackgroundColor(segment), expanded: !this._isCollapsed(segment), depth: segment.depth, children: [], }; // map the children to their contexts if (segment.children) { context.children = segment.children.map(this._getContext.bind(this)); } return context; } trackByIndex(index) { return index; } /** Convert the public facing data structure into the layout format we require */ setDataset(dataset) { // convert the segments to a hierarchichal structure const segmentHierarchy = hierarchy(dataset).sum(this.getSegmentValue); // calculate segment values based on their children // store the processed segments const root = partition()(segmentHierarchy); // store the flattened form of the segments this._segments = root.descendants(); // mark the root node as focusable this._focusableSegment = root; // we need to run change detection here so the `*ngFor` will update and add all the segments to the DOM this._changeDetector.detectChanges(); // select all the segments within the chart this._segmentsSelection = select(this._elementRef.nativeElement) .selectAll('.partition-map-segment') .data(this._segments); // set the correct sizing and position of the segments this.updateSegments(); // if there is an item waiting to be selected then select it if (this._awaitingSelection) { // select the desired segment this.select(this.getHierarchyNodeFromSegment(this._awaitingSelection)); // clear the pending selection in case the dataset changes we don't want to attempt another selection this._awaitingSelection = null; } } /** Update the size and position of the segments */ updateSegments() { // if the chart has not yet been initialised do nothing if (!this._segmentsSelection) { return; } // clear the segment cache this._segmentCache = new WeakMap(); // perform the chart positioning and sizing this._segmentsSelection .style('left', data => this.getNormalizedSegmentX(data) + '%') .style('top', data => this.getNormalizedSegmentY(data) + '%') .style('width', data => this.getNormalizedSegmentWidth(data) + 0.01 + '%') .style('height', data => this.getNormalizedSegmentHeight(data) + '%') .style('padding-right', data => this.getSegmentPaddingRight(data) + '%') .style('padding-left', data => this.getSegmentPaddingLeft(data) + '%'); } cacheSegment(segment, data) { // get any existing cache data const existing = this._segmentCache.get(segment) ?? {}; // store the new data this._segmentCache.set(segment, { ...existing, ...data }); } getSegmentCache(segment, key) { return this._segmentCache.get(segment)?.[key]; } /** * Get the X position of a given segment. The X position can be determined * by calculating the width of every sibling segment to the left of it */ getSegmentX(segment) { // if root node then return the position if (!segment.parent) { return segment.x0; } const cachedX = this.getSegmentCache(segment.data, 'x'); if (cachedX !== undefined) { return cachedX; } // set initial start position equal to that of the parent let accumulation = this.getSegmentX(segment.parent); // iterate each previous sibling to accumulate the widths for (const sibling of segment.parent.children) { // if we have reached the current node then return all previous widths if (sibling === segment) { this.cacheSegment(segment.data, { x: accumulation }); return accumulation; } // keep a tally of all the widths of previous siblings accumulation += this.getSegmentWidth(sibling); } } /** Calculate width based of each segment */ getSegmentWidth(segment) { // if root node then return 1 always if (!segment.parent) { return 1; } const cachedWidth = this.getSegmentCache(segment.data, 'width'); if (cachedWidth !== undefined) { return cachedWidth; } // get width of parent const parentOffset = this.getSegmentWidth(segment.parent) / (segment.parent.x1 - segment.parent.x0); // get the original width of the segment const width = segment.x1 - segment.x0; // if the item is a descendant of the selected item then apply the modifier if (this.isDescendantOfSelected(segment)) { // we want to try an ensure that children are at least the specified minimum width // however it may not always be possible, but we should be able to at least distribute the widths better // even if we cannot meet the minimum desired width. const modifier = this.getDistributionModifier(segment); // return the width of the current node relative to the parent const result = width * modifier * parentOffset; // store the result in the cache this.cacheSegment(segment.data, { width: result }); return result; } const result = width * parentOffset; // store the result in the cache this.cacheSegment(segment.data, { width: result }); return result; } /** Return the X position of the segment in a normalized form based on the specifiec domain */ getNormalizedSegmentX(segment) { return this._x(this.getSegmentX(segment)); } /** Return the Y position of the segment in a normalized form based on the specifiec domain */ getNormalizedSegmentY(segment) { // if there is a selected node we should take into account any collapsed nodes if (this._isCollapsed(segment)) { return segment.depth * this.getCollapsedHeight(); } // otherwise simply return the normalized value return this._y(segment.y0); } /** Return the width of the segment in a normalized form based on the specifiec domain */ getNormalizedSegmentWidth(segment) { return (this._x(this.getSegmentX(segment) + this.getSegmentWidth(segment)) - this._x(this.getSegmentX(segment))); } /** Return the height of the segment in a normalized form based on the specifiec domain */ getNormalizedSegmentHeight(segment) { // if there is a selected node we should take into account any collapsed nodes if (this._isCollapsed(segment)) { return this.getCollapsedHeight(); } // otherwise simply return the normalized value return this._y(segment.y0 + (segment.y1 - segment.y0)) - this._y(segment.y0); } /** * As parent segments collapse they increase in size, as the content is centered this can * cause the content to appear either mis-aligned or off screen. We can calculate the padding * required to always ensure the content appears visibly centered within the node. */ getSegmentPaddingRight(segment) { // non-collapsed node do not require any padding if (!this._isCollapsed(segment)) { return 0; } return (this.getNormalizedSegmentWidth(segment) - this.getSegmentPaddingLeft(segment) - this.getNormalizedSegmentWidth(this._selected)); } getSegmentPaddingLeft(segment) { // non-collapsed node do not require any padding if (!this._isCollapsed(segment)) { return 0; } return Math.abs(this.getNormalizedSegmentX(segment)); } /** * This function returns the value for each segment. Leaf segments will have a value property which we can simply return, however * non-leaf segments should get their values based on the leaf segments that are children, in which case we can return 0 */ getSegmentValue(segment) { // eslint-disable-next-line no-prototype-builtins if (segment.hasOwnProperty('value')) { const value = segment.value; // we must ensure that a leaf node never has no width otherwise things can get weird return Math.max(value, 1); } // if it has children then return 0 to base the value of the width of the children return 0; } /** Get the total height of all the collapse rows */ getTotalCollapsedHeight() { return this._selected ? this._selected.depth * this.getCollapsedHeight() : 0; } /** Get the collapsed height in percentage format */ getCollapsedHeight() { return parseFloat(((this.collapsedHeight / this._height) * 100).toPrecision(3)); } /** Determine if a given segment is currently visible based on the selected segment */ isVisible(segment) { // if no segment is selected then all segments are visible if (!this._selected) { return true; } // if there is a selected node then it should be a direct ancestor or descendant to be visible return !![...this._selected.ancestors(), ...this._selected.descendants()].find(_segment => _segment === segment); } /** Update the focusable item and perform a focus */ focusSegment(segment) { // get the segment element from the data const element = this._segmentsSelection .nodes() .find(node => select(node).data()[0] === segment); // if for some reason an element isn't found then stop here if (!element) { return; } // update the focusable segment this._focusableSegment = segment; // set the focus origin as a keyboard event this._focusOrigin.setOrigin('keyboard'); // focus the element element.focus(); // ensure we do not change scroll position when focusing this._elementRef.nativeElement.scrollLeft = 0; this._elementRef.nativeElement.scrollTop = 0; } /** Get all the segments at a given depth */ getAllSiblings(segment) { return this._segments.filter(_segment => _segment.depth === segment.depth); } getHierarchyNodeFromSegment(segment) { return this._segments.find(_segment => _segment.data === segment); } /** Select a specified segment */ select(segment) { // if no segment is specified or it is already selected then do nothing if (!segment || this._isSelected(segment)) { return; } // clear the segment cache this._segmentCache = new WeakMap(); // emit the selection this.selectedChange.emit(segment.data); // store the selected segment this._selected = segment; // update the focusable segment this._focusableSegment = segment; // set our new ranges this._x.domain([ this.getSegmentX(segment), this.getSegmentX(segment) + this.getSegmentWidth(segment), ]); this._y.domain([segment.y0, 1]).range([this.getTotalCollapsedHeight(), 100]); // create the transition const segmentTransition = transition('organizationChartSegmentTransition').duration(500); // update the segment sizes - outside angular zone as there is lots of `requestAnimationFrames` triggering lots of change detection this._ngZone.runOutsideAngular(() => { this._segmentsSelection .transition(segmentTransition) .style('left', data => this.getNormalizedSegmentX(data) + '%') .style('top', data => this.getNormalizedSegmentY(data) + '%') .style('width', data => this.getNormalizedSegmentWidth(data) + 0.01 + '%') .style('height', data => this.getNormalizedSegmentHeight(data) + '%') .style('padding-right', data => this.getSegmentPaddingRight(data) + '%') .style('padding-left', data => this.getSegmentPaddingLeft(data) + '%'); }); } /** Normalize the available colors to a string[][] from portentially a ThemeColor[][] */ getColorSequence(depth) { // get the target row const colorSet = this._colors[depth]; // if no color set available throw an error if (!colorSet) { throw new Error('Partition Map: Please provide a color sequence for items with a depth of ' + depth); } // convert this row to an array of strings return colorSet.map(color => ThemeColor.isInstanceOf(color) ? color.toRgba() : this._colorService.resolve(color)); } /** Determine if a segment is a descendant of the currently selected item */ isDescendantOfSelected(segment) { // if there are no segments selected then return true if (!this._selected) { return true; } // if the segment is the selected segment then it is not a descendant if (this._selected === segment) { return false; } return !!this._selected.descendants().find(_segment => _segment === segment); } /** * We have an option to allow a minimum desired width for items. This will * allow us to attempt to determine the size a segment would be accounting for very * small segments that have their widths artifically increased to make them more visible */ getDistributionModifier(segment) { // calculate the desired number of pixels as a percentage const minSegmentWidth = (this.minSegmentWidth / this._width) * 100; // map to a segment width pair const siblings = segment.parent.children.map(_segment => { return { segment: _segment, width: this._x(_segment.x1 - _segment.x0) }; }); // a simple closure to check if we now have acceptable sizes const isAcceptable = (segments) => !segments.find(_segment => _segment.width < minSegmentWidth) || segments.filter(_segment => _segment.width < minSegmentWidth).length === siblings.length; // if all segments are above or below the desired width then we can stop here if (isAcceptable(siblings)) { return 1; } // find the total amount we need to reclaim for other segments let amountToReclaim = siblings.reduce((accumulation, _segment) => accumulation + (_segment.width < minSegmentWidth ? minSegmentWidth - _segment.width : 0), 0); // loop through adjusting the segments until we either make all acceptable sizes or cannot resize any further while (!isAcceptable(siblings) && amountToReclaim !== 0) { // determine which segments can shrink const shrinkableSiblings = siblings.filter(sibling => sibling.width > minSegmentWidth); // determine which segments need to grow const growableSiblings = siblings.filter(sibling => sibling.width < minSegmentWidth); // if there are no items that can be shrunk/grown then do nothing if (shrinkableSiblings.length === 0 || growableSiblings.length === 0) { break; } // determine the target amount to remove from each segment const shrinkTarget = amountToReclaim / shrinkableSiblings.length; // store the amount we have reclaimed in this pass let reclaimed = 0; // iterate each segment and subtract accordingly for (const sibling of shrinkableSiblings) { // determine how much we can actually subtract - as subtracting the target may bring the width down below the // minimum which we don't want, so instead determine if we can subtract the target amount, otherwise figure out // how much we can subtract without bringing the width below the desired minimum const subtractAmount = sibling.width - shrinkTarget > minSegmentWidth ? shrinkTarget : sibling.width - minSegmentWidth; // update the amount to reclaim with the new value reclaimed += subtractAmount; // update the sibling width sibling.width -= subtractAmount; } // update the amount left to reclaim amountToReclaim -= reclaimed; // determine the target amount to add to each segment const growTarget = reclaimed / growableSiblings.length; // add the available reclaimed amount to the segment that need to grow for (const sibling of growableSiblings) { // determine the amount we need to add. The target amount may be larger than the amount we need // to add so ensure we only add the amount we need and no more. const addAmount = sibling.width + growTarget < minSegmentWidth ? growTarget : minSegmentWidth - sibling.width; // update the sibling width sibling.width += addAmount; } } // identify the current widget from all the siblings const matchingSegment = siblings.find(sibling => sibling.segment === segment); // check if we are the last sibling const isLast = siblings.findIndex(sibling => sibling.segment === segment) === siblings.length - 1; // if we are the last and somehow we are smaller than the parent node, we want to bump up the size of the last node if (isLast) { // get the total parent width const parentWidth = this._x(segment.parent.x1 - segment.parent.x0); // get the total width of all the children const width = siblings.reduce((total, sibling) => total + sibling.width, 0); // check if need to expand the last node if (parentWidth !== width) { return ((matchingSegment.width + (parentWidth - width)) / this._x(matchingSegment.segment.x1 - matchingSegment.segment.x0)); } } // determine the amount the size has changed return matchingSegment.width / this._x(matchingSegment.segment.x1 - matchingSegment.segment.x0); } /** Get the default announcement when a segment is focused */ defaultSegmentAnnouncement(info) { // create the announcement if (info.parents.length === 0) { return `This is the root segment. It has a value of ${info.value}.`; } // otherwise inform the user of the parent hierarchy return `${info.item.name} has a value of ${info.value} and is a ${info.parents.map(parent => `descendant of ${parent.name}`).join(' and a ')}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.12", ngImport: i0, type: PartitionMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.12", type: PartitionMapComponent, selector: "ux-partition-map", inputs: { colors: "colors", collapsedHeight: "collapsedHeight", minSegmentWidth: "minSegmentWidth", dataset: "dataset", selected: "selected", segmentAnnouncement: "segmentAnnouncement" }, outputs: { selectedChange: "selectedChange" }, host: { attributes: { "role": "tree", "aria-orientation": "vertical" } }, queries: [{ propertyName: "segmentTemplate", first: true, predicate: ["partitionMapSegment"], descendants: true }], ngImport: i0, template: "@for (segment of _segments; track trackByIndex($index)) {\n <div class=\"partition-map-segment\"\n uxFocusIndicator\n [ngClass]=\"_getContrastColor(segment)\"\n [style.background-color]=\"_getBackgroundColor(segment)\"\n [tabIndex]=\"_getTabIndex(segment)\"\n (click)=\"_onSegmentSelect(segment)\"\n (focus)=\"_onFocus(segment)\"\n role=\"treeitem\"\n [attr.aria-expanded]=\"!_isCollapsed(segment)\"\n [attr.aria-selected]=\"_isSelected(segment)\"\n [attr.aria-level]=\"segment.depth + 1\"\n [attr.aria-label]=\"segment.data.name\"\n (keydown.Enter)=\"_onSegmentSelect(segment)\"\n (keydown.ArrowUp)=\"_focusParent(segment); $event.preventDefault()\"\n (keydown.ArrowDown)=\"_focusChild(segment); $event.preventDefault()\"\n (keydown.ArrowLeft)=\"_focusSibling(segment, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"_focusSibling(segment, 1); $event.preventDefault()\"\n (keydown.Home)=\"_focusFirstSibling(segment); $event.preventDefault()\"\n (keydown.End)=\"_focusLastSibling(segment); $event.preventDefault()\">\n <div class=\"partition-map-segment-content\" [class.partition-map-segment-content-hidden]=\"_getSegmentContentHidden(segment)\">\n <!-- Show default template if provided -->\n @if (!segmentTemplate) {\n <span class=\"partition-map-segment-label\">\n {{ segment.data.name }}\n </span>\n }\n <!-- Show custom template if provided -->\n @if (segmentTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"segmentTemplate\"\n [ngTemplateOutletContext]=\"_getContext(segment)\">\n </ng-container>\n }\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: i1.FocusIndicatorDirective, selector: "[uxFocusIndicator]", inputs: ["checkChildren", "mouseFocusIndicator", "touchFocusIndicator", "keyboardFocusIndicator", "programmaticFocusIndicator"], outputs: ["indicator"], exportAs: ["ux-focus-indicator"] }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.12", ngImport: i0, type: PartitionMapComponent, decorators: [{ type: Component, args: [{ selector: 'ux-partition-map', changeDetection: ChangeDetectionStrategy.OnPush, host: { role: 'tree', 'aria-orientation': 'vertical', }, template: "@for (segment of _segments; track trackByIndex($index)) {\n <div class=\"partition-map-segment\"\n uxFocusIndicator\n [ngClass]=\"_getContrastColor(segment)\"\n [style.background-color]=\"_getBackgroundColor(segment)\"\n [tabIndex]=\"_getTabIndex(segment)\"\n (click)=\"_onSegmentSelect(segment)\"\n (focus)=\"_onFocus(segment)\"\n role=\"treeitem\"\n [attr.aria-expanded]=\"!_isCollapsed(segment)\"\n [attr.aria-selected]=\"_isSelected(segment)\"\n [attr.aria-level]=\"segment.depth + 1\"\n [attr.aria-label]=\"segment.data.name\"\n (keydown.Enter)=\"_onSegmentSelect(segment)\"\n (keydown.ArrowUp)=\"_focusParent(segment); $event.preventDefault()\"\n (keydown.ArrowDown)=\"_focusChild(segment); $event.preventDefault()\"\n (keydown.ArrowLeft)=\"_focusSibling(segment, -1); $event.preventDefault()\"\n (keydown.ArrowRight)=\"_focusSibling(segment, 1); $event.preventDefault()\"\n (keydown.Home)=\"_focusFirstSibling(segment); $event.preventDefault()\"\n (keydown.End)=\"_focusLastSibling(segment); $event.preventDefault()\">\n <div class=\"partition-map-segment-content\" [class.partition-map-segment-content-hidden]=\"_getSegmentContentHidden(segment)\">\n <!-- Show default template if provided -->\n @if (!segmentTemplate) {\n <span class=\"partition-map-segment-label\">\n {{ segment.data.name }}\n </span>\n }\n <!-- Show custom template if provided -->\n @if (segmentTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"segmentTemplate\"\n [ngTemplateOutletContext]=\"_getContext(segment)\">\n </ng-container>\n }\n </div>\n </div>\n}\n" }] }], propDecorators: { colors: [{ type: Input }], collapsedHeight: [{ type: Input }], minSegmentWidth: [{ type: Input }], dataset: [{ type: Input }], selected: [{ type: Input }], segmentAnnouncement: [{ type: Input }], selectedChange: [{ type: Output }], segmentTemplate: [{ type: ContentChild, args: ['partitionMapSegment', { static: false }] }] } }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGFydGl0aW9uLW1hcC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvY29tcG9uZW50cy9wYXJ0aXRpb24tbWFwL3BhcnRpdGlvbi1tYXAuY29tcG9uZW50LnRzIiwiLi4vLi4vLi4vLi4vLi4vc3JjL2NvbXBvbmVudHMvcGFydGl0aW9uLW1hcC9wYXJ0aXRpb24tbWFwLmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDREQUE0RDtBQUM1RCxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDbEQsT0FBTyxFQUNMLHVCQUF1QixFQUN2QixpQkFBaUIsRUFDakIsU0FBUyxFQUNULFlBQVksRUFDWixVQUFVLEVBQ1YsWUFBWSxFQUNaLE1BQU0sRUFDTixLQUFLLEVBQ0wsTUFBTSxFQUdOLE1BQU0sR0FFUCxNQUFNLGVBQWUsQ0FBQztBQUN2QixPQUFPLEVBQ0wsU0FBUyxFQUVULFNBQVMsRUFDVCxXQUFXLEVBQ1gsTUFBTSxFQUVOLFVBQVUsR0FDWCxNQUFNLElBQUksQ0FBQztBQUNaLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxNQUFNLENBQUM7QUFDL0IsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQzNDLE9BQU8sRUFBRSxlQUFlLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxzQ0FBc0MsQ0FBQztBQUNwRyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDOUQsT0FBTyxFQUFFLFlBQVksRUFBRSxVQUFVLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQzs7OztBQWdCdEUsTUFBTSxPQUFPLHFCQUFxQjtJQVRsQztRQVVtQixrQkFBYSxHQUFHLE1BQU0sQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUVyQyxnQkFBVyxHQUFHLE1BQU0sQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUVqQyxvQkFBZSxHQUFHLE1BQU0sQ0FBQyxp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