@ux-aspects/ux-aspects
Version:
Open source user interface framework for building modern, responsive, mobile big data applications
685 lines • 116 kB
JavaScript
/* 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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