2

I inherited some markup where a series of top-level <a> elements each contain a set of <span> elements, and using CSS, they're rendered as clickable blocks in a list, like this:

.list {
  display: inline-flex;
  flex-flow: column nowrap;
  font: 14px Arial;
}
  .list a {
    display: flex;
    flex-flow: column nowrap;
    align-items: stretch;
    border: 1px solid #CCC;
    border-bottom: none;
    background: #FFF;
    padding: 4px 10px;
    text-decoration: none;
    color: #000;
  }
    .list a:last-child {
      border-bottom: 1px solid #CCC;
    }
    .list a:hover {
      background: #CDE;
    }

    .list a .name {
      font-weight: bold;
    }
    .list a .secondary {
      display: flex;
      flex-flow: row nowrap;
      justify-content: space-between;
      color: #678;
      font-size: 85%;
      padding-top: 2px;
    }
    
    .list a .address {
      padding-right: 16px;
      padding-left: 8px;
    }
    .list a .company-id {
      color: #B88;
      cursor: text;
      padding-left: 4px;
      padding-right: 4px;
      margin-right: -4px;
    }
<div class="list">
    <a href="/link/to/company/10101">
        <span class="name">Alice Jones &amp; Co.</span>
        <span class="secondary">
            <span class="address">55 Oak Street, Anytown 15151</span>
            <span class="company-id">#10101</span>
        </span>
    </a>
    <a href="/link/to/company/12345">
        <span class="name">John Smith Inc.</span>
        <span class="secondary">
            <span class="address">123 Main Street, Anytown 15151</span>
            <span class="company-id">#12345</span>
        </span>
    </a>
    <a href="/link/to/company/20123">
        <span class="name">Bob Johnson LLC</span>
        <span class="secondary">
            <span class="address">17 Spruce Street, Anytown 15152</span>
            <span class="company-id">#20123</span>
        </span>
    </a>
</div>

The Request

A product owner asked me the other day if I could make the company IDs not clickable — our users want to be able to select the text of the IDs for copy-and-paste. Fine, I thought: Turn each <a> element into an <li> like it should be anyway, add a little JavaScript to follow the links on clicks, and ignore clicks on the company IDs, and done.

Then I learned there's another user requirement — that the <a> elements must also be middle-clickable or Ctrl-clickable to open them in a new tab. I intended to tweak the JavaScript to invoke window.open() if the Ctrl-key or middle-mouse button was down, but it seems that ad-blockers and browser popup blockers get in the way of that working reliably: The <a> element needs to be a real <a> element, and its events must be left more-or-less untouched. But that means that the <a> will capture every bubbling event on its content, including events I'd prefer it not touch, like the click-and-drag (and double-click) events on the company ID.

And since the list has a flexible layout, I can't put the company ID element outside the <a> element, and then appear to make it part of the same block using position or margin tricks: The spacing won't work, because the IDs vary pretty widely in length (from 1 to 129370-5486).

tl;dr: I need a child element to exist inside an <a> element for layout — but it needs to exist outside the same element for behavior.


Requirements

For a valid solution, I have to meet these requirements:

  • The full <a> element must be clickable as a link, except for the company ID <span>.
  • The full <a> element can be middle-clicked to open it in a new tab, except for the company ID <span>.
  • A user must be able to click-and-drag on the company ID <span> to select and copy its text.
  • A user must be able to double-click on the company ID <span> to select and copy its text.
  • The layout must be flexible, allowing text spans of arbitrary length, and collapsing to the narrowest overall width.
  • The solution must work in modern evergreen browsers (i.e., Chrome, Firefox, Edge — no old-IE compatibility required!).

Beyond that, the sky's the limit: Dependencies, no dependencies, add/tweak the CSS, add some JS, change the markup — as long as those six bullet points are met, you can do whatever you want.


My Best Solution

I've tried an awful lot of JavaScript event-capturing tricks so far, most of which were failures. The best working solution I've found involves no JS at all: I include the company ID in the markup twice — once inside the <a> with visibility:hidden for layout purposes, and then again in the markup after the </a>, with a position:relative-containing <li> element around all of it, and position:absolute / bottom: / right: on the visible, selectable <span>. But it seems like there ought to be a better way that doesn't involve mutating the markup; and if the product owners ever want more text in each box, or a slightly different layout, my solution is not likely to adjust to those changes very well.

So do you have any better ideas than I have for pulling off normal, selectable text elements inside an otherwise-clickable <a> element parent?

Asons
  • 84,923
  • 12
  • 110
  • 165
Sean Werkema
  • 5,810
  • 2
  • 38
  • 42

2 Answers2

3

Heydon Pickering wrote an article on inclusive card design where it goes over how to handle selectable text inside a card component where the entire card is clickable.

One of the solutions that would work for you would be to change each of the <a> elements into <li> elements as you had, but then add an <a> tag around just the name of the company. Then you can add a pseudo element to the anchor tag that expands the full width/height of the parent <li>. Lastly, add position: relative to the id to bring it above the anchors pseudo element so it won't activate the link.

.list {
  display: inline-flex;
  flex-flow: column nowrap;
  font: 14px Arial;
  margin: 0;
  padding: 0;
}
  .list li {
    display: flex;
    flex-flow: column nowrap;
    align-items: stretch;
    border: 1px solid #CCC;
    border-bottom: none;
    background: #FFF;
    padding: 4px 10px;
    text-decoration: none;
    color: #000;
    position: relative;
  }
    .list a::after {
      content: '';
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
    }
  
    .list li:last-child {
      border-bottom: 1px solid #CCC;
    }
    .list li:hover {
      background: #CDE;
    }

    .list li .name {
      font-weight: bold;
    }
    .list li .secondary {
      display: flex;
      flex-flow: row nowrap;
      justify-content: space-between;
      color: #678;
      font-size: 85%;
      padding-top: 2px;
    }
    
    .list li .address {
      padding-right: 16px;
      padding-left: 8px;
    }
    .list li .company-id {
      color: #B88;
      cursor: text;
      padding-left: 4px;
      padding-right: 4px;
      margin-right: -4px;
      position: relative;
    }
<ul class="list">
    <li>
        <a href="/link/to/company/10101" class="name">Alice Jones &amp; Co.</a>
        <span class="secondary">
            <span class="address">55 Oak Street, Anytown 15151</span>
            <span class="company-id">#10101</span>
        </span>
    </li>
    <li>
      <a href="/link/to/company/12345" class="name">John Smith Inc.</a>
      <span class="secondary">
          <span class="address">123 Main Street, Anytown 15151</span>
          <span class="company-id">#12345</span>
      </span>
    </li>
    <li>
      <a href="/link/to/company/20123" class="name">Bob Johnson LLC</a>
      <span class="secondary">
          <span class="address">17 Spruce Street, Anytown 15152</span>
          <span class="company-id">#20123</span>
      </span>
    </li>
</ul>
Steven Lambert
  • 5,571
  • 2
  • 29
  • 46
  • 1
    This is perfect! Thanks for the link too ;) – Don Dec 05 '18 at 19:21
  • I love that stretched pseudo-element trick! I tend to forget that you can use top/bottom/left/right to stretch elements, not just position them. This is hands-down the best solution I've seen so far: The markup is clean, the CSS has very few oddities, and the whole thing is pure HTML-and-CSS. Nicely done :) – Sean Werkema Dec 05 '18 at 20:14
0

Here is the related codepen I created but the psuedo-a is commented out in the js: https://codepen.io/anon/pen/XyLgag

This should get you started but not exactly as you need I'm sure.... HTH

CSS:

a.psuedo {
  position: absolute;
  top: 0;
  left: 30%;
  width: 15px;
  height: 15px;
  border: 0 !important;
  background: transparent;
  margin: 0;
  padding: 0;
}

JQUERY TO COPY TO CLIPBOARD:

//Add custom event listener
$(':root').on('mousedown', '*', function() {
    var el = $(this),
        events = $._data(this, 'events');
    if (events && events.clickHold) {
        el.data(
            'clickHoldTimer',
            setTimeout(
                function() {
                    el.trigger('clickHold')
                },
                el.data('clickHoldTimeout')
            )
        );
    }
}).on('mouseup mouseleave mousemove', '*', function() {
    clearTimeout($(this).data('clickHoldTimer'));
});

//Attach it to the element
$('.company-id').data('clickHoldTimeout', 2000); //Time to hold
$('.company-id').on('clickHold', function() {
  /* Get the text field */
  var copyText = $(this).html();

  /* Copy the text inside the text field */
  document.execCommand("copy");
  console.log("copied -- " + copyText);

});

JQUERY TO ADD PSEUDO-ANCHOR TAG

$(".company-id").each(function() {
    $(this).append('<a class="psuedo" target="_blank" href="www.google.com"></a>');
  });
Fraze
  • 908
  • 2
  • 8
  • 20